This commit is contained in:
Flatlogic Bot 2025-10-25 22:28:34 +00:00
parent cd996ba56a
commit e483e6e608
53 changed files with 1150 additions and 188 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -141,6 +141,9 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_DIRS = [ STATICFILES_DIRS = [
BASE_DIR / 'static', BASE_DIR / 'static',

View File

@ -16,8 +16,13 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", include("core.urls")), path("", include("core.urls")),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,8 +1,49 @@
from django.contrib import admin from django.contrib import admin
from .models import Ticket from .models import Ticket, Client, Portfolio, Activity, Document
import secrets
import string
@admin.register(Ticket) @admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin): class TicketAdmin(admin.ModelAdmin):
list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at') list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at')
list_filter = ('status', 'priority') list_filter = ('status', 'priority')
search_fields = ('subject', 'requester_email', 'description') search_fields = ('subject', 'requester_email', 'description')
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
list_display = ('name', 'surname', 'email', 'phone', 'created_at')
search_fields = ('name', 'surname', 'email')
readonly_fields = ('generated_password',)
fieldsets = (
(None, {
'fields': ('name', 'surname', 'email', 'phone', 'address', 'fiscal_code', 'vat_number')
}),
('Credenziali', {
'fields': ('generated_password',)
}),
)
def save_model(self, request, obj, form, change):
if not obj.pk: # Only generate password for new clients
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for i in range(12))
obj.generated_password = password
super().save_model(request, obj, form, change)
@admin.register(Portfolio)
class PortfolioAdmin(admin.ModelAdmin):
list_display = ('name', 'client', 'created_at')
search_fields = ('name', 'client__name', 'client__surname')
list_filter = ('client',)
@admin.register(Activity)
class ActivityAdmin(admin.ModelAdmin):
list_display = ('title', 'client', 'status', 'activity_date', 'created_at')
search_fields = ('title', 'client__name', 'client__surname')
list_filter = ('status', 'activity_date', 'client')
@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
list_display = ('title', 'client', 'created_at')
search_fields = ('title', 'client__name', 'client__surname')
list_filter = ('client',)

View File

@ -1,7 +1,31 @@
from django import forms from django import forms
from .models import Ticket from .models import Document, Client, Portfolio, Activity
class TicketForm(forms.ModelForm): class DocumentForm(forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Document
fields = ['subject', 'requester_email', 'priority', 'description'] fields = ('client', 'title', 'description', 'file')
widgets = {
'client': forms.Select(attrs={'class': 'form-control'}),
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
}
class ClientForm(forms.ModelForm):
class Meta:
model = Client
fields = ('name', 'email', 'phone', 'address')
class PortfolioForm(forms.ModelForm):
class Meta:
model = Portfolio
fields = ('client', 'name', 'description')
class ActivityForm(forms.ModelForm):
class Meta:
model = Activity
fields = ('client', 'title', 'description', 'status', 'activity_date')

View File

@ -0,0 +1,34 @@
# Generated by Django 5.2.7 on 2025-10-25 21:35
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='Nome')),
('surname', models.CharField(max_length=100, verbose_name='Cognome')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefono')),
('address', models.CharField(blank=True, max_length=255, null=True, verbose_name='Indirizzo')),
('fiscal_code', models.CharField(blank=True, max_length=16, null=True, verbose_name='Codice Fiscale')),
('vat_number', models.CharField(blank=True, max_length=11, null=True, verbose_name='Partita IVA')),
('generated_password', models.CharField(help_text='Password generata per il cliente (visibile in chiaro)', max_length=128)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Cliente',
'verbose_name_plural': 'Clienti',
},
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2025-10-25 21:42
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_client'),
]
operations = [
migrations.CreateModel(
name='Portfolio',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='Nome Portafoglio')),
('description', models.TextField(blank=True, null=True, verbose_name='Descrizione')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='portfolios', to='core.client', verbose_name='Cliente')),
],
options={
'verbose_name': 'Portafoglio',
'verbose_name_plural': 'Portafogli',
},
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-25 21:44
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_portfolio'),
]
operations = [
migrations.CreateModel(
name='Activity',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=200, verbose_name='Titolo')),
('description', models.TextField(blank=True, null=True, verbose_name='Descrizione')),
('status', models.CharField(choices=[('todo', 'To Do'), ('in_progress', 'In Progress'), ('done', 'Done')], default='todo', max_length=20, verbose_name='Stato')),
('activity_date', models.DateField(verbose_name='Data Attività')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='core.client', verbose_name='Cliente')),
],
options={
'verbose_name': 'Attività',
'verbose_name_plural': 'Attività',
'ordering': ['-activity_date'],
},
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-25 21:46
import core.models
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_activity'),
]
operations = [
migrations.CreateModel(
name='Document',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=200, verbose_name='Titolo')),
('description', models.TextField(blank=True, null=True, verbose_name='Descrizione')),
('file', models.FileField(upload_to=core.models.client_directory_path, verbose_name='File')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.client', verbose_name='Cliente')),
],
options={
'verbose_name': 'Documento',
'verbose_name_plural': 'Documenti',
'ordering': ['-created_at'],
},
),
]

View File

@ -1,4 +1,5 @@
from django.db import models from django.db import models
import uuid
class Ticket(models.Model): class Ticket(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
@ -23,3 +24,83 @@ class Ticket(models.Model):
def __str__(self): def __str__(self):
return self.subject return self.subject
class Client(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100, verbose_name="Nome")
surname = models.CharField(max_length=100, verbose_name="Cognome")
email = models.EmailField(unique=True, verbose_name="Email")
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="Telefono")
address = models.CharField(max_length=255, blank=True, null=True, verbose_name="Indirizzo")
fiscal_code = models.CharField(max_length=16, blank=True, null=True, verbose_name="Codice Fiscale")
vat_number = models.CharField(max_length=11, blank=True, null=True, verbose_name="Partita IVA")
generated_password = models.CharField(max_length=128, help_text="Password generata per il cliente (visibile in chiaro)")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Cliente"
verbose_name_plural = "Clienti"
def __str__(self):
return f"{self.name} {self.surname}"
class Portfolio(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100, verbose_name="Nome Portafoglio")
description = models.TextField(blank=True, null=True, verbose_name="Descrizione")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='portfolios', verbose_name="Cliente")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Portafoglio"
verbose_name_plural = "Portafogli"
def __str__(self):
return f"{self.name} ({self.client})"
class Activity(models.Model):
STATUS_CHOICES = [
('todo', 'To Do'),
('in_progress', 'In Progress'),
('done', 'Done'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200, verbose_name="Titolo")
description = models.TextField(blank=True, null=True, verbose_name="Descrizione")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='activities', verbose_name="Cliente")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='todo', verbose_name="Stato")
activity_date = models.DateField(verbose_name="Data Attività")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Attività"
verbose_name_plural = "Attività"
ordering = ['-activity_date']
def __str__(self):
return self.title
def client_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/documents/<client_id>/<filename>
return f'documents/{instance.client.id}/{filename}'
class Document(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200, verbose_name="Titolo")
description = models.TextField(blank=True, null=True, verbose_name="Descrizione")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='documents', verbose_name="Cliente")
file = models.FileField(upload_to=client_directory_path, verbose_name="File")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Documento"
verbose_name_plural = "Documenti"
ordering = ['-created_at']
def __str__(self):
return self.title

55
core/templates/base.html Normal file
View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Finance Hub{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<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=Lato:wght@400;700&display=swap" rel="stylesheet">
{% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
<style>
body {
font-family: 'Lato', sans-serif;
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">Financial Hub</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{% url 'client_list' %}">Clienti</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'portfolio_list' %}">Portfolio</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'activity_list' %}">Attività</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'document_list' %}">Documenti</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<main class="col-md-12 ms-sm-auto col-lg-12 px-md-4">
{% block content %}{% endblock %}
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>Delete Activity</h2>
<p>Are you sure you want to delete "{{ object }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Confirm Delete</button>
<a href="{% url 'activity_detail' pk=object.pk %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Dettaglio Attività</h1>
<div>
<a href="{% url 'activity_update' activity.pk %}" class="btn btn-secondary">Modifica</a>
<a href="{% url 'activity_delete' activity.pk %}" class="btn btn-danger">Elimina</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>{{ activity.title }}</h3>
</div>
<div class="card-body">
<p><strong>Cliente:</strong> <a href="{% url 'client_detail' activity.client.pk %}">{{ activity.client.name }}</a></p>
<p><strong>Stato:</strong> <span class="badge bg-primary">{{ activity.get_status_display }}</span></p>
<p><strong>Descrizione:</strong></p>
<p>{{ activity.description|linebreaksbr }}</p>
<p><strong>Data Creazione:</strong> {{ activity.created_at|date:"d/m/Y H:i" }}</p>
</div>
<div class="card-footer text-muted">
<a href="{% url 'activity_list' %}" class="btn btn-light">Torna alla lista</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>{{ title }}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
{% if form.instance.pk %}
<a href="{% url 'activity_detail' pk=form.instance.pk %}" class="btn btn-secondary">Cancel</a>
{% else %}
<a href="{% url 'activity_list' %}" class="btn btn-secondary">Cancel</a>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Attività</h1>
<a href="{% url 'activity_create' %}" class="btn btn-primary">Aggiungi Attività</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Titolo</th>
<th scope="col">Cliente</th>
<th scope="col">Stato</th>
<th scope="col">Data Creazione</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for activity in activities %}
<tr>
<td>{{ activity.title }}</td>
<td><a href="{% url 'client_detail' activity.client.pk %}">{{ activity.client.name }}</a></td>
<td><span class="badge bg-primary">{{ activity.get_status_display }}</span></td>
<td>{{ activity.created_at|date:"d/m/Y" }}</td>
<td>
<a href="{% url 'activity_detail' activity.pk %}" class="btn btn-sm btn-outline-primary">Dettagli</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">Nessuna attività trovata.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block title %}{{ article.title }}{% endblock %}
{% block content %}
<div class="container mt-5">
<h1>{{ article.title }}</h1>
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
<hr>
<div>
{{ article.content|safe }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>Delete Client</h2>
<p>Are you sure you want to delete "{{ object }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Confirm Delete</button>
<a href="{% url 'client_detail' pk=object.pk %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Dettaglio Cliente</h1>
<div>
<a href="{% url 'client_update' client.pk %}" class="btn btn-secondary">Modifica</a>
<a href="{% url 'client_delete' client.pk %}" class="btn btn-danger">Elimina</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h3>{{ client.name }}</h3>
</div>
<div class="card-body">
<p><strong>Email:</strong> {{ client.email }}</p>
<p><strong>Telefono:</strong> {{ client.phone }}</p>
<p><strong>Indirizzo:</strong> {{ client.address }}</p>
<p><strong>Password di sistema:</strong> <code>{{ client.system_password }}</code></p>
<p><strong>Data Creazione:</strong> {{ client.created_at|date:"d/m/Y H:i" }}</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h4>Attività del Cliente</h4>
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Titolo</th>
<th>Stato</th>
<th>Data</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for activity in activities %}
<tr>
<td>{{ activity.title }}</td>
<td><span class="badge bg-secondary">{{ activity.get_status_display }}</span></td>
<td>{{ activity.activity_date|date:"d/m/Y" }}</td>
<td>
<a href="{% url 'activity_detail' activity.pk %}" class="btn btn-sm btn-info">Dettagli</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center">Nessuna attività trovata per questo cliente.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer text-muted">
<a href="{% url 'client_list' %}" class="btn btn-light">Torna alla lista</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>{{ title }}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
{% if form.instance.pk %}
<a href="{% url 'client_detail' pk=form.instance.pk %}" class="btn btn-secondary">Cancel</a>
{% else %}
<a href="{% url 'client_list' %}" class="btn btn-secondary">Cancel</a>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Clienti</h1>
<a href="{% url 'client_create' %}" class="btn btn-primary">Aggiungi Cliente</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Email</th>
<th scope="col">Telefono</th>
<th scope="col">Data Creazione</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for client in clients %}
<tr>
<td>{{ client.name }}</td>
<td>{{ client.email }}</td>
<td>{{ client.phone }}</td>
<td>{{ client.created_at|date:"d/m/Y" }}</td>
<td>
<a href="{% url 'client_detail' client.pk %}" class="btn btn-sm btn-outline-primary">Dettagli</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">Nessun cliente trovato.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Dettaglio Documento</h1>
<div>
<a href="{% url 'document_delete' document.pk %}" class="btn btn-danger">Elimina</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>{{ document.name }}</h3>
</div>
<div class="card-body">
<p><strong>Cliente:</strong> <a href="{% url 'client_detail' document.client.pk %}">{{ document.client.name }}</a></p>
<p><strong>Descrizione:</strong></p>
<p>{{ document.description|linebreaksbr }}</p>
<p><strong>Data Caricamento:</strong> {{ document.created_at|date:"d/m/Y H:i" }}</p>
<p><strong>File:</strong> <a href="{{ document.file.url }}" target="_blank">Visualizza/Scarica</a></p>
</div>
<div class="card-footer text-muted">
<a href="{% url 'document_list' %}" class="btn btn-light">Torna alla lista</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Documenti</h1>
<a href="{% url 'document_upload' %}" class="btn btn-primary">Carica Documento</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Cliente</th>
<th scope="col">Data Caricamento</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for document in documents %}
<tr>
<td>{{ document.name }}</td>
<td><a href="{% url 'client_detail' document.client.pk %}">{{ document.client.name }}</a></td>
<td>{{ document.created_at|date:"d/m/Y" }}</td>
<td>
<a href="{% url 'document_detail' document.pk %}" class="btn btn-sm btn-outline-primary">Dettagli</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center">Nessun documento trovato.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h1 class="card-title">Carica Nuovo Documento</h1>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label for="id_client" class="form-label">Cliente</label>
{{ form.client }}
</div>
<div class="mb-3">
<label for="id_title" class="form-label">Titolo</label>
{{ form.title }}
</div>
<div class="mb-3">
<label for="id_description" class="form-label">Descrizione</label>
{{ form.description }}
</div>
<div class="mb-3">
<label for="id_file" class="form-label">File</label>
{{ form.file }}
</div>
<button type="submit" class="btn btn-primary">Carica</button>
<a href="{% url 'document_list' %}" class="btn btn-secondary">Annulla</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,157 +1,94 @@
<!doctype html> {% extends 'base.html' %}
<html lang="en">
<head> {% block title %}Dashboard - Finance Hub{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ project_name }}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
<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;600;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.08);
--card-border-color: rgba(255, 255, 255, 0.18);
}
* { {% block content %}
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(130deg, 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 { <!-- KPI Cards -->
content: ''; <div class="row">
position: absolute; <div class="col-md-3 mb-3">
inset: 0; <div class="card kpi-card">
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'><path d='M-20 20L160 20M20 -20L20 160' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>"); <div class="card-body">
animation: bg-pan 24s linear infinite; <h5 class="card-title">Totale Clienti</h5>
z-index: -1; <p class="display-4">{{ kpi_data.total_clients }}</p>
} </div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card kpi-card">
<div class="card-body">
<h5 class="card-title">Totale Portafogli</h5>
<p class="display-4">{{ kpi_data.total_portfolios }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card kpi-card">
<div class="card-body">
<h5 class="card-title">Totale Attività</h5>
<p class="display-4">{{ kpi_data.total_activities }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card kpi-card">
<div class="card-body">
<h5 class="card-title">Totale Documenti</h5>
<p class="display-4">{{ kpi_data.total_documents }}</p>
</div>
</div>
</div>
</div>
@keyframes bg-pan { <!-- Widget Grid -->
0% { <div class="row mt-4">
transform: translate3d(0, 0, 0); <div class="col-md-6 mb-4">
}
100% {
transform: translate3d(-140px, -140px, 0);
}
}
main {
padding: clamp(2rem, 4vw, 3rem);
width: min(640px, 92vw);
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 20px;
padding: clamp(2rem, 4vw, 3rem);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
}
h1 {
margin: 0 0 1.2rem;
font-weight: 700;
font-size: clamp(2.2rem, 3vw + 1.3rem, 3rem);
letter-spacing: -0.04em;
}
p {
margin: 0.6rem 0;
font-size: 1.1rem;
line-height: 1.7;
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);
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
code {
background: rgba(15, 23, 42, 0.35);
padding: 0.2rem 0.6rem;
border-radius: 0.5rem;
font-size: 0.95rem;
}
footer {
margin-top: 2.4rem;
font-size: 0.86rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card"> <div class="card">
<h1>Analyzing your requirements and generating your website…</h1> <div class="card-header d-flex justify-content-between align-items-center">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> Clienti Recenti
<span class="sr-only">Loading…</span> <a href="{% url 'client_list' %}" class="btn btn-sm btn-outline-primary">Vedi Tutti</a>
</div> </div>
<p>Appwizzy AI is collecting your requirements and applying the first changes.</p> <div class="card-body">
<p>This page will refresh automatically as the plan is implemented.</p> {% if recent_clients %}
<p> <ul class="list-group list-group-flush">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> {% for client in recent_clients %}
UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code> <li class="list-group-item d-flex justify-content-between align-items-center">
</p> {{ client.name }} {{ client.surname }}
<a href="{% url 'client_detail' client.pk %}" class="btn btn-sm btn-light">Dettagli</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">Nessun cliente recente.</p>
{% endif %}
</div> </div>
<footer> </div>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) </div>
</footer> <div class="col-md-6 mb-4">
</main> <div class="card">
</body> <div class="card-header d-flex justify-content-between align-items-center">
Attività Recenti
</html> <a href="{% url 'activity_list' %}" class="btn btn-sm btn-outline-primary">Vedi Tutte</a>
</div>
<div class="card-body">
{% if recent_activities %}
<ul class="list-group list-group-flush">
{% for activity in recent_activities %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ activity.title }}</strong><br>
<small class="text-muted">{{ activity.client }} - {{ activity.activity_date|date:"d M Y" }}</small>
</div>
<a href="{% url 'activity_detail' activity.pk %}" class="btn btn-sm btn-light">Dettagli</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">Nessuna attività recente.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>Delete Portfolio</h2>
<p>Are you sure you want to delete "{{ object }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Confirm Delete</button>
<a href="{% url 'portfolio_detail' pk=object.pk %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Dettaglio Portafoglio</h1>
<div>
<a href="{% url 'portfolio_update' portfolio.pk %}" class="btn btn-secondary">Modifica</a>
<a href="{% url 'portfolio_delete' portfolio.pk %}" class="btn btn-danger">Elimina</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>{{ portfolio.name }}</h3>
</div>
<div class="card-body">
<p><strong>Cliente:</strong> <a href="{% url 'client_detail' portfolio.client.pk %}">{{ portfolio.client.name }}</a></p>
<p><strong>Descrizione:</strong></p>
<p>{{ portfolio.description|linebreaksbr }}</p>
<p><strong>Data Creazione:</strong> {{ portfolio.created_at|date:"d/m/Y H:i" }}</p>
</div>
<div class="card-footer text-muted">
<a href="{% url 'portfolio_list' %}" class="btn btn-light">Torna alla lista</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h2>{{ title }}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
{% if form.instance.pk %}
<a href="{% url 'portfolio_detail' pk=form.instance.pk %}" class="btn btn-secondary">Cancel</a>
{% else %}
<a href="{% url 'portfolio_list' %}" class="btn btn-secondary">Cancel</a>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Portafogli</h1>
<a href="{% url 'portfolio_create' %}" class="btn btn-primary">Aggiungi Portafoglio</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Cliente</th>
<th scope="col">Data Creazione</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for portfolio in portfolios %}
<tr>
<td>{{ portfolio.name }}</td>
<td>{{ portfolio.client.name }}</td>
<td>{{ portfolio.created_at|date:"d/m/Y" }}</td>
<td>
<a href="{% url 'portfolio_detail' portfolio.pk %}" class="btn btn-sm btn-outline-primary">Dettagli</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center">Nessun portafoglio trovato.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,47 @@
from django.urls import path from django.urls import path
from .views import home from .views import (
index,
client_list,
client_detail,
client_create,
client_update,
client_delete,
portfolio_list,
portfolio_detail,
portfolio_create,
portfolio_update,
portfolio_delete,
activity_list,
activity_detail,
activity_create,
activity_update,
activity_delete,
document_list,
document_detail,
document_upload,
document_delete
)
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path("", index, name="home"),
path("clients/", client_list, name="client_list"),
path("clients/add/", client_create, name="client_create"),
path("clients/<uuid:pk>/", client_detail, name="client_detail"),
path("clients/<uuid:pk>/edit/", client_update, name="client_update"),
path("clients/<uuid:pk>/delete/", client_delete, name="client_delete"),
path("portfolios/", portfolio_list, name="portfolio_list"),
path("portfolios/add/", portfolio_create, name="portfolio_create"),
path("portfolios/<uuid:pk>/", portfolio_detail, name="portfolio_detail"),
path("portfolios/<uuid:pk>/edit/", portfolio_update, name="portfolio_update"),
path("portfolios/<uuid:pk>/delete/", portfolio_delete, name="portfolio_delete"),
path("activities/", activity_list, name="activity_list"),
path("activities/add/", activity_create, name="activity_create"),
path("activities/<uuid:pk>/", activity_detail, name="activity_detail"),
path("activities/<uuid:pk>/edit/", activity_update, name="activity_update"),
path("activities/<uuid:pk>/delete/", activity_delete, name="activity_delete"),
path("documents/", document_list, name="document_list"),
path("documents/upload/", document_upload, name="document_upload"),
path("documents/<uuid:pk>/", document_detail, name="document_detail"),
path("documents/<uuid:pk>/delete/", document_delete, name="document_delete"),
] ]

View File

@ -1,37 +1,206 @@
import os from django.shortcuts import render, get_object_or_404, redirect
import platform from .models import Client, Portfolio, Activity, Document
from .forms import DocumentForm, ClientForm, PortfolioForm, ActivityForm
from django import get_version as django_version def index(request):
from django.shortcuts import render """Render the main dashboard screen."""
from django.urls import reverse_lazy # Placeholder data for the KPI cards
from django.utils import timezone kpi_data = {
from django.views.generic.edit import CreateView "total_clients": Client.objects.count(),
"total_portfolios": Portfolio.objects.count(),
"total_activities": Activity.objects.count(),
"total_documents": Document.objects.count(),
"pending_tasks": Activity.objects.filter(status='todo').count(),
}
from .forms import TicketForm # Fetch recent items for the dashboard widgets
from .models import Ticket recent_clients = Client.objects.order_by('-created_at')[:5]
recent_activities = Activity.objects.order_by('-created_at')[:5]
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()
context = { context = {
"project_name": "New Style", "project_name": "Finance Hub",
"agent_brand": agent_brand, "kpi_data": kpi_data,
"django_version": django_version(), "recent_clients": recent_clients,
"python_version": platform.python_version(), "recent_activities": recent_activities,
"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 client_list(request):
"""Render a page with a list of all clients."""
clients = Client.objects.all()
context = {
"clients": clients,
}
return render(request, "core/client_list.html", context)
class TicketCreateView(CreateView): def client_detail(request, pk):
model = Ticket """Render a page with the details of a single client."""
form_class = TicketForm client = get_object_or_404(Client, pk=pk)
template_name = "core/ticket_create.html" activities = client.activities.all()
success_url = reverse_lazy("home") context = {
"client": client,
"activities": activities,
}
return render(request, "core/client_detail.html", context)
def portfolio_list(request):
"""Render a page with a list of all portfolios."""
portfolios = Portfolio.objects.all()
context = {
"portfolios": portfolios,
}
return render(request, "core/portfolio_list.html", context)
def portfolio_detail(request, pk):
"""Render a page with the details of a single portfolio."""
portfolio = get_object_or_404(Portfolio, pk=pk)
context = {
"portfolio": portfolio,
}
return render(request, "core/portfolio_detail.html", context)
def activity_list(request):
"""Render a page with a list of all activities."""
activities = Activity.objects.all()
context = {
"activities": activities,
}
return render(request, "core/activity_list.html", context)
def activity_detail(request, pk):
"""Render a page with the details of a single activity."""
activity = get_object_or_404(Activity, pk=pk)
context = {
"activity": activity,
}
return render(request, "core/activity_detail.html", context)
def document_list(request):
"""Render a page with a list of all documents."""
documents = Document.objects.all()
context = {
"documents": documents,
}
return render(request, "core/document_list.html", context)
def document_detail(request, pk):
"""Render a page with the details of a single document."""
document = get_object_or_404(Document, pk=pk)
context = {
"document": document,
}
return render(request, "core/document_detail.html", context)
def document_upload(request):
"""Handle document upload."""
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('document_list')
else:
form = DocumentForm()
context = {
'form': form,
}
return render(request, 'core/document_upload.html', context)
def client_create(request):
if request.method == 'POST':
form = ClientForm(request.POST)
if form.is_valid():
form.save()
return redirect('client_list')
else:
form = ClientForm()
return render(request, 'core/client_form.html', {'form': form, 'title': 'Add Client'})
def client_update(request, pk):
client = get_object_or_404(Client, pk=pk)
if request.method == 'POST':
form = ClientForm(request.POST, instance=client)
if form.is_valid():
form.save()
return redirect('client_detail', pk=client.pk)
else:
form = ClientForm(instance=client)
return render(request, 'core/client_form.html', {'form': form, 'title': 'Edit Client'})
def client_delete(request, pk):
client = get_object_or_404(Client, pk=pk)
if request.method == 'POST':
client.delete()
return redirect('client_list')
return render(request, 'core/client_confirm_delete.html', {'object': client})
def portfolio_create(request):
if request.method == 'POST':
form = PortfolioForm(request.POST)
if form.is_valid():
form.save()
return redirect('portfolio_list')
else:
form = PortfolioForm()
return render(request, 'core/portfolio_form.html', {'form': form, 'title': 'Add Portfolio'})
def portfolio_update(request, pk):
portfolio = get_object_or_404(Portfolio, pk=pk)
if request.method == 'POST':
form = PortfolioForm(request.POST, instance=portfolio)
if form.is_valid():
form.save()
return redirect('portfolio_detail', pk=portfolio.pk)
else:
form = PortfolioForm(instance=portfolio)
return render(request, 'core/portfolio_form.html', {'form': form, 'title': 'Edit Portfolio'})
def portfolio_delete(request, pk):
portfolio = get_object_or_404(Portfolio, pk=pk)
if request.method == 'POST':
portfolio.delete()
return redirect('portfolio_list')
return render(request, 'core/portfolio_confirm_delete.html', {'object': portfolio})
def activity_create(request):
if request.method == 'POST':
form = ActivityForm(request.POST)
if form.is_valid():
form.save()
return redirect('activity_list')
else:
form = ActivityForm()
return render(request, 'core/activity_form.html', {'form': form, 'title': 'Add Activity'})
def activity_update(request, pk):
activity = get_object_or_404(Activity, pk=pk)
if request.method == 'POST':
form = ActivityForm(request.POST, instance=activity)
if form.is_valid():
form.save()
return redirect('activity_detail', pk=activity.pk)
else:
form = ActivityForm(instance=activity)
return render(request, 'core/activity_form.html', {'form': form, 'title': 'Edit Activity'})
def activity_delete(request, pk):
activity = get_object_or_404(Activity, pk=pk)
if request.method == 'POST':
activity.delete()
return redirect('activity_list')
return render(request, 'core/activity_confirm_delete.html', {'object': activity})
def document_delete(request, pk):
document = get_object_or_404(Document, pk=pk)
if request.method == 'POST':
document.delete()
return redirect('document_list')
return render(request, 'core/document_confirm_delete.html', {'object': document})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

31
static/css/custom.css Normal file
View File

@ -0,0 +1,31 @@
/* Your custom CSS goes here */
:root {
--bs-primary: #2196F3;
--bs-primary-rgb: 33, 150, 243;
}
body {
background-color: #F8F9FA;
}
.btn-primary {
--bs-btn-bg: #2196F3;
--bs-btn-border-color: #2196F3;
--bs-btn-hover-bg: #1a78c2;
--bs-btn-hover-border-color: #1a78c2;
}
.kpi-card .card-body {
text-align: center;
}
.kpi-card h5 {
font-size: 1.1rem;
color: #6c757d;
}
.kpi-card .display-4 {
font-weight: 700;
color: #343A40;
}