diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..8ccc282 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 0b85e94..ea61c96 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..3a184a9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -180,3 +180,6 @@ if EMAIL_USE_SSL: # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_REDIRECT_URL = 'dashboard' +LOGOUT_REDIRECT_URL = 'login' diff --git a/config/urls.py b/config/urls.py index bcfc074..fecf250 100644 --- a/config/urls.py +++ b/config/urls.py @@ -21,9 +21,10 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("", include("core.urls")), ] if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..8463b1e 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..44ec54c Binary files /dev/null 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 e061640..934d1a3 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 5a69659..35266b8 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 2a36fd6..d7dfe7b 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 8c38f3f..3a0401b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,13 @@ from django.contrib import admin +from .models import Ticket, Comment -# Register your models here. +@admin.register(Ticket) +class TicketAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'status', 'priority', 'category', 'created_by', 'created_at') + list_filter = ('status', 'priority', 'category') + search_fields = ('title', 'description') + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ('ticket', 'author', 'created_at') + list_filter = ('created_at',) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..08dce08 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,21 @@ +from django import forms +from .models import Ticket, Comment + +class TicketForm(forms.ModelForm): + class Meta: + model = Ticket + fields = ['title', 'description', 'priority', 'category'] + widgets = { + 'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Brief summary of the issue'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'Describe your problem in detail...'}), + 'priority': forms.Select(attrs={'class': 'form-select'}), + 'category': forms.Select(attrs={'class': 'form-select'}), + } + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + fields = ['text'] + widgets = { + 'text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Type your reply here...'}), + } diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..0d6058f --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.7 on 2026-02-16 11:01 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Ticket', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('status', models.CharField(choices=[('open', 'Open'), ('pending', 'Pending'), ('resolved', 'Resolved'), ('closed', 'Closed')], default='open', max_length=20)), + ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='medium', max_length=20)), + ('category', models.CharField(choices=[('technical', 'Technical Issue'), ('billing', 'Billing'), ('feature', 'Feature Request'), ('other', 'Other')], default='technical', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tickets', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-updated_at'], + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.ticket')), + ], + options={ + 'ordering': ['created_at'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..bdb0ea9 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..8cf848d 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,52 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. +class Ticket(models.Model): + STATUS_CHOICES = [ + ('open', 'Open'), + ('pending', 'Pending'), + ('resolved', 'Resolved'), + ('closed', 'Closed'), + ] + PRIORITY_CHOICES = [ + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High'), + ('urgent', 'Urgent'), + ] + CATEGORY_CHOICES = [ + ('technical', 'Technical Issue'), + ('billing', 'Billing'), + ('feature', 'Feature Request'), + ('other', 'Other'), + ] + + title = models.CharField(max_length=200) + description = models.TextField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') + priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='technical') + + created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tickets') + assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_tickets') + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"#{self.id} - {self.title}" + + class Meta: + ordering = ['-updated_at'] + +class Comment(models.Model): + ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments') + author = models.ForeignKey(User, on_delete=models.CASCADE) + text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Comment by {self.author} on {self.ticket}" + + class Meta: + ordering = ['created_at'] \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..0155d6e 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,79 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Support Center{% endblock %} + + + + + + + + {% block extra_head %}{% endblock %} - - {% block content %}{% endblock %} - + - +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% block content %}{% endblock %} +
+ + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..3f8d784 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,84 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}Dashboard - Support Center{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Support Dashboard

+

Manage and track your support requests

-

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" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +
+
+
+

{{ open_count }}

+ Open Tickets +
+
+
+
+

{{ pending_count }}

+ Pending +
+
+
+
+

{{ resolved_count }}

+ Resolved +
+
+
+ +
+
+
+ + + + + + + + + + + + {% for ticket in tickets %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
TicketStatusPriorityUpdatedAction
+
{{ ticket.title }}
+
#{{ ticket.id }} • {{ ticket.get_category_display }}
+
+ + {{ ticket.get_status_display }} + + + + {{ ticket.get_priority_display }} + + + {{ ticket.updated_at|timesince }} ago + + View +
+
+

No tickets found. Need help? Create a new ticket!

+ Create First Ticket +
+
+
+
+{% endblock %} diff --git a/core/templates/core/ticket_detail.html b/core/templates/core/ticket_detail.html new file mode 100644 index 0000000..a3e25ca --- /dev/null +++ b/core/templates/core/ticket_detail.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} +{% block title %}Ticket #{{ ticket.id }} - Support Center{% endblock %} + +{% block content %} +
+
+ + + +
+
+
+ + {{ ticket.get_status_display }} + +

{{ ticket.title }}

+

+ Submitted by {{ ticket.created_by.username }} • + {{ ticket.created_at|date:"F j, Y, g:i a" }} +

+
+
+ #{{ ticket.id }} +
+
+ +
+ +
+

{{ ticket.description }}

+
+
+ + +

Discussion

+
+ {% for comment in ticket.comments.all %} +
+
+ + {{ comment.author.username }} + {% if comment.author.is_staff %}(Agent){% endif %} + + {{ comment.created_at|timesince }} ago +
+

{{ comment.text }}

+
+ {% empty %} +

No replies yet.

+ {% endfor %} +
+ + +
+
Add a Reply
+
+ {% csrf_token %} +
+ {{ comment_form.text }} +
+
+ +
+
+
+
+ + +
+
+
Ticket Details
+ +
+ + {{ ticket.get_priority_display }} +
+ +
+ + {{ ticket.get_category_display }} +
+ +
+ + {{ ticket.assigned_to.username|default:"Unassigned" }} +
+ + {% if user.is_staff %} +
+
Manage Status
+
+ {% csrf_token %} +
+ +
+ +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/ticket_form.html b/core/templates/core/ticket_form.html new file mode 100644 index 0000000..96d8e8e --- /dev/null +++ b/core/templates/core/ticket_form.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}New Ticket - Support Center{% endblock %} + +{% block content %} +
+
+ + +
+

Submit a Ticket

+

Please fill out the form below and our team will get back to you as soon as possible.

+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors|first }}
+ {% endif %} +
+ {% endfor %} + +
+ +
+
+
+
+
+{% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..bec6415 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} + +{% block title %}Login - Support Center{% endblock %} + +{% block content %} +
+
+
+
+

Welcome Back

+

Please log in to manage your support tickets.

+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+ + {% if form.errors %} +
+ Your username and password didn't match. Please try again. +
+ {% endif %} + + + +
+ +
+

Don't have an account? Contact your administrator.

+
+
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..da1ea0b 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,8 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path("", views.dashboard, name="dashboard"), + path("ticket/new/", views.ticket_create, name="ticket_create"), + path("ticket//", views.ticket_detail, name="ticket_detail"), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..b1eb88b 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,71 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - - -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() +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from .models import Ticket, Comment +from .forms import TicketForm, CommentForm +@login_required +def dashboard(request): + """User dashboard showing their tickets.""" + if request.user.is_staff: + tickets = Ticket.objects.all() + else: + tickets = Ticket.objects.filter(created_by=request.user) + 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", ""), + 'tickets': tickets, + 'open_count': tickets.filter(status='open').count(), + 'pending_count': tickets.filter(status='pending').count(), + 'resolved_count': tickets.filter(status='resolved').count(), } - return render(request, "core/index.html", context) + return render(request, 'core/index.html', context) + +@login_required +def ticket_create(request): + """View to create a new ticket.""" + if request.method == 'POST': + form = TicketForm(request.POST) + if form.is_valid(): + ticket = form.save(commit=False) + ticket.created_by = request.user + ticket.save() + messages.success(request, "Ticket created successfully!") + return redirect('dashboard') + else: + form = TicketForm() + + return render(request, 'core/ticket_form.html', {'form': form}) + +@login_required +def ticket_detail(request, pk): + """View ticket details and handle comments.""" + ticket = get_object_or_404(Ticket, pk=pk) + + # Simple permission check + if not request.user.is_staff and ticket.created_by != request.user: + messages.error(request, "You do not have permission to view this ticket.") + return redirect('dashboard') + + if request.method == 'POST': + # Check if it's a comment or a status update + if 'status' in request.POST and request.user.is_staff: + ticket.status = request.POST.get('status') + ticket.save() + messages.success(request, f"Status updated to {ticket.get_status_display()}") + return redirect('ticket_detail', pk=pk) + + comment_form = CommentForm(request.POST) + if comment_form.is_valid(): + comment = comment_form.save(commit=False) + comment.ticket = ticket + comment.author = request.user + comment.save() + messages.success(request, "Reply added!") + return redirect('ticket_detail', pk=pk) + else: + comment_form = CommentForm() + + return render(request, 'core/ticket_detail.html', { + 'ticket': ticket, + 'comment_form': comment_form, + }) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..9e2ea4c 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,85 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Poppins:wght@500;600;700&display=swap'); + +:root { + --primary-color: #0984E3; + --success-color: #00B894; + --warning-color: #FDCB6E; + --danger-color: #D63031; + --bg-color: #F9FAFB; + --card-bg: rgba(255, 255, 255, 0.95); + --text-main: #2D3436; + --text-muted: #636E72; } + +body { + font-family: 'Inter', sans-serif; + background-color: var(--bg-color); + color: var(--text-main); + line-height: 1.6; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Poppins', sans-serif; + font-weight: 600; +} + +.navbar { + background: var(--card-bg); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.btn-primary { + background-color: var(--primary-color); + border: none; + padding: 0.6rem 1.5rem; + border-radius: 8px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn-primary:hover { + background-color: #0873C4; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(9, 132, 227, 0.2); +} + +.card { + border: none; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0,0,0,0.03); + background: var(--card-bg); + transition: transform 0.2s ease; +} + +.card-ticket:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(0,0,0,0.06); +} + +.status-badge { + padding: 0.4rem 0.8rem; + border-radius: 50px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-open { background: rgba(9, 132, 227, 0.1); color: var(--primary-color); } +.status-pending { background: rgba(253, 203, 110, 0.1); color: #E17055; } +.status-resolved { background: rgba(0, 184, 148, 0.1); color: var(--success-color); } +.status-closed { background: rgba(99, 110, 114, 0.1); color: var(--text-muted); } + +.priority-high { color: var(--danger-color); font-weight: 600; } +.priority-urgent { color: #D63031; text-decoration: underline; } + +.thread-comment { + border-left: 3px solid #dfe6e9; + margin-bottom: 1.5rem; + padding-left: 1.5rem; +} + +.thread-comment.agent-reply { + border-left-color: var(--primary-color); +} \ No newline at end of file