diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8bcb2fb Binary files /dev/null and b/ai/__pycache__/__init__.cpython-311.pyc differ diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc new file mode 100644 index 0000000..fa9977d Binary files /dev/null and b/ai/__pycache__/local_ai_api.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..a5ebf5c 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 9aa598b..af9630e 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 1f807fa..e6775df 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 6867ddf..aae512f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..2ce8565 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,13 @@ +from django import forms +from .models import Task + +class TaskForm(forms.ModelForm): + class Meta: + model = Task + fields = ['title', 'category', 'due_date', 'due_time'] + widgets = { + 'title': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Cosa devi fare?'}), + 'category': forms.Select(attrs={'class': 'form-select'}), + 'due_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), + 'due_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}), + } diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8c466d9 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/send_due_date_reminders.py b/core/management/commands/send_due_date_reminders.py new file mode 100644 index 0000000..375adb8 --- /dev/null +++ b/core/management/commands/send_due_date_reminders.py @@ -0,0 +1,37 @@ +import os +from django.core.management.base import BaseCommand +from django.core.mail import send_mail +from django.utils import timezone +from core.models import Task + +class Command(BaseCommand): + help = 'Sends email reminders for tasks that are due today.' + + def handle(self, *args, **options): + today = timezone.now().date() + due_tasks = Task.objects.filter(due_date=today, status__in=['DA_FARE', 'IN_SOSPESO']) + + if not due_tasks: + self.stdout.write(self.style.SUCCESS('No tasks are due today.')) + return + + recipient_list = os.environ.get('CONTACT_EMAIL_TO', '').split(',') + if not recipient_list or not recipient_list[0]: + self.stdout.write(self.style.ERROR('No recipient email configured. Please set CONTACT_EMAIL_TO environment variable.')) + return + + for task in due_tasks: + subject = f'Reminder: Task "{task.title}" is due today!' + message = f'Hello,\n\nThis is a reminder that your task "{task.title}" is due today, {today}.\n\nCategory: {task.get_category_display()}\nStatus: {task.get_status_display()}\n\nThank you!' + + try: + send_mail( + subject, + message, + os.environ.get('DEFAULT_FROM_EMAIL'), + recipient_list, + fail_silently=False, + ) + self.stdout.write(self.style.SUCCESS(f'Successfully sent reminder for task: "{task.title}"')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'Failed to send email for task: "{task.title}". Error: {e}')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..85a8fd0 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-01-07 21:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='Titolo')), + ('description', models.TextField(blank=True, null=True, verbose_name='Descrizione')), + ('due_date', models.DateField(blank=True, null=True, verbose_name='Data di Scadenza')), + ('priority', models.CharField(choices=[('L', 'Bassa'), ('M', 'Media'), ('H', 'Alta')], default='M', max_length=1, verbose_name='Priorità')), + ('category', models.CharField(blank=True, max_length=100, verbose_name='Categoria')), + ('completed', models.BooleanField(default=False, verbose_name='Completato')), + ], + options={ + 'verbose_name': 'Attività', + 'verbose_name_plural': 'Attività', + 'ordering': ['due_date'], + }, + ), + ] diff --git a/core/migrations/0002_alter_task_options_remove_task_completed_and_more.py b/core/migrations/0002_alter_task_options_remove_task_completed_and_more.py new file mode 100644 index 0000000..4f20dd6 --- /dev/null +++ b/core/migrations/0002_alter_task_options_remove_task_completed_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.7 on 2026-01-07 21:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='task', + options={'ordering': ['status', 'category'], 'verbose_name': 'Attività', 'verbose_name_plural': 'Attività'}, + ), + migrations.RemoveField( + model_name='task', + name='completed', + ), + migrations.RemoveField( + model_name='task', + name='description', + ), + migrations.RemoveField( + model_name='task', + name='due_date', + ), + migrations.RemoveField( + model_name='task', + name='priority', + ), + migrations.AddField( + model_name='task', + name='status', + field=models.CharField(choices=[('DA_FARE', 'Da Fare'), ('IN_SOSPESO', 'In Sospeso'), ('COMPLETATO', 'Completato')], default='DA_FARE', max_length=10, verbose_name='Stato'), + ), + migrations.AlterField( + model_name='task', + name='category', + field=models.CharField(choices=[('LAVORO', 'Lavoro'), ('FAMIGLIA', 'Famiglia'), ('PERSONALE', 'Personale')], default='PERSONALE', max_length=10, verbose_name='Categoria'), + ), + migrations.AlterField( + model_name='task', + name='title', + field=models.TextField(verbose_name='Cosa devi fare?'), + ), + ] diff --git a/core/migrations/0003_task_due_date_task_due_time.py b/core/migrations/0003_task_due_date_task_due_time.py new file mode 100644 index 0000000..d16039d --- /dev/null +++ b/core/migrations/0003_task_due_date_task_due_time.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-01-07 21:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_task_options_remove_task_completed_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='due_date', + field=models.DateField(blank=True, null=True, verbose_name='Data di scadenza'), + ), + migrations.AddField( + model_name='task', + name='due_time', + field=models.TimeField(blank=True, null=True, verbose_name='Ora di scadenza'), + ), + ] 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..0dca65a Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_alter_task_options_remove_task_completed_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_alter_task_options_remove_task_completed_and_more.cpython-311.pyc new file mode 100644 index 0000000..6947e82 Binary files /dev/null and b/core/migrations/__pycache__/0002_alter_task_options_remove_task_completed_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_task_due_date_task_due_time.cpython-311.pyc b/core/migrations/__pycache__/0003_task_due_date_task_due_time.cpython-311.pyc new file mode 100644 index 0000000..8d0aa54 Binary files /dev/null and b/core/migrations/__pycache__/0003_task_due_date_task_due_time.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..89604f1 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,28 @@ from django.db import models -# Create your models here. +class Task(models.Model): + CATEGORY_CHOICES = [ + ('LAVORO', 'Lavoro'), + ('FAMIGLIA', 'Famiglia'), + ('PERSONALE', 'Personale'), + ] + + STATUS_CHOICES = [ + ('DA_FARE', 'Da Fare'), + ('IN_SOSPESO', 'In Sospeso'), + ('COMPLETATO', 'Completato'), + ] + + title = models.TextField(verbose_name="Cosa devi fare?") + category = models.CharField(max_length=10, choices=CATEGORY_CHOICES, default='PERSONALE', verbose_name="Categoria") + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='DA_FARE', verbose_name="Stato") + due_date = models.DateField(verbose_name="Data di scadenza", null=True, blank=True) + due_time = models.TimeField(verbose_name="Ora di scadenza", null=True, blank=True) + + def __str__(self): + return self.title + + class Meta: + ordering = ['status', 'category'] + verbose_name = "Attività" + verbose_name_plural = "Attività" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..35cfad9 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -14,12 +14,17 @@ {% endif %} {% load static %} + + + + {% block head %}{% endblock %} {% block content %}{% endblock %} + diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..ae5f473 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,323 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% load static custom_filters %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… + + +
+
+ +
+ +
+
+
Roma
+

Il tuo assistente esecutivo

+ +
+
+ + +
+
+
Impostazioni Timer
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+
Aggiungi Attività
+
+ {% csrf_token %} +
{{ form.title.label_tag }}{{ form.title }}
+
{{ form.category.label_tag }}{{ form.category }}
+
+
{{ form.due_date.label_tag }}{{ form.due_date }}
+
{{ form.due_time.label_tag }}{{ form.due_time }}
+
+
+
+
+
+
+ + +
+ {% for category_value, category_display in categories %} +
+

{{ category_display }}

+
+ {% for status_id, status_name in statuses %} +
+
+
{{ status_name }}
+ {% for task in tasks|get_item:status_id|get_item:category_value %} +
+
+
{{ task.title }}
+ {% if task.due_date %}

{{ task.due_date|date:"d M" }}{% if task.due_time %} alle {{ task.due_time|time:"H:i" }}{% endif %}

{% endif %} + +
25:00
+ +
+ {% if status_id != 'COMPLETATO' %} + + Modifica + {% endif %} +
+ +
+ {% if status_id == 'DA_FARE' %} + Sospendi + Completa + {% elif status_id == 'IN_SOSPESO' %} + Da Fare + Completa + {% else %} + Elimina + {% endif %} +
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% endfor %} +
+
+
+ + +
- + + + + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/task_edit.html b/core/templates/core/task_edit.html new file mode 100644 index 0000000..835675a --- /dev/null +++ b/core/templates/core/task_edit.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}Modifica Attività{% endblock %} + +{% block content %} +
+
+
+
+
+

Modifica Attività: {{ task.title }}

+
+ {% csrf_token %} + {{ form.as_p }} + + Annulla +
+
+
+
+
+
+{% endblock %} diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/__pycache__/__init__.cpython-311.pyc b/core/templatetags/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..d8b27f6 Binary files /dev/null and b/core/templatetags/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/templatetags/__pycache__/custom_filters.cpython-311.pyc b/core/templatetags/__pycache__/custom_filters.cpython-311.pyc new file mode 100644 index 0000000..1567d6a Binary files /dev/null and b/core/templatetags/__pycache__/custom_filters.cpython-311.pyc differ diff --git a/core/templatetags/custom_filters.py b/core/templatetags/custom_filters.py new file mode 100644 index 0000000..4b04b5c --- /dev/null +++ b/core/templatetags/custom_filters.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter +def get_item(dictionary, key): + return dictionary.get(key) diff --git a/core/urls.py b/core/urls.py index 6299e3d..feb6db8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,17 @@ from django.urls import path -from .views import home +from .views import ( + index, task_edit, task_delete, + task_set_status_completato, task_set_status_in_sospeso, task_set_status_da_fare, + assistant_chat +) urlpatterns = [ - path("", home, name="home"), + path("", index, name="index"), + path("edit//", task_edit, name="task_edit"), + path("delete//", task_delete, name="task_delete"), + path("task/complete//", task_set_status_completato, name="task_set_status_completato"), + path("task/in-sospeso//", task_set_status_in_sospeso, name="task_set_status_in_sospeso"), + path("task/da-fare//", task_set_status_da_fare, name="task_set_status_da_fare"), + path('assistant-chat/', assistant_chat, name='assistant_chat'), ] diff --git a/core/views.py b/core/views.py index c9aed12..20aaf8b 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,98 @@ -import os -import platform +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Task +from .forms import TaskForm +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from ai.local_ai_api import LocalAIApi +import json -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +def assistant_chat(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + message = data.get('message') + if not message: + return JsonResponse({'error': 'No message provided'}, status=400) -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() + # Call the Local AI API + response = LocalAIApi.create_response( + { + "input": [ + {"role": "system", "content": "You are a helpful assistant named Roma."}, + {"role": "user", "content": message}, + ], + } + ) + + if response.get("success"): + ai_reply = LocalAIApi.extract_text(response) + if not ai_reply: + decoded = LocalAIApi.decode_json_from_response(response) + ai_reply = json.dumps(decoded, ensure_ascii=False) if decoded else str(response.get("data", "")) + return JsonResponse({'reply': ai_reply}) + else: + return JsonResponse({'error': response.get("error", "Unknown AI error")}, status=500) + + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON'}, status=400) + + return JsonResponse({'error': 'Only POST method is allowed'}, status=405) + +def index(request): + if request.method == 'POST': + form = TaskForm(request.POST) + if form.is_valid(): + form.save() + return redirect('index') + else: + form = TaskForm() + + # Prepara una struttura dati nidificata per il template + tasks_by_status_cat = {status[0]: {cat[0]: [] for cat in Task.CATEGORY_CHOICES} for status in Task.STATUS_CHOICES} + all_tasks = Task.objects.all() + + for task in all_tasks: + tasks_by_status_cat[task.status][task.category].append(task) 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", ""), + 'form': form, + 'tasks': tasks_by_status_cat, + 'categories': Task.CATEGORY_CHOICES, + 'statuses': Task.STATUS_CHOICES, } - return render(request, "core/index.html", context) + return render(request, 'core/index.html', context) + +def task_edit(request, pk): + task = get_object_or_404(Task, pk=pk) + if request.method == 'POST': + form = TaskForm(request.POST, instance=task) + if form.is_valid(): + form.save() + return redirect('index') + else: + form = TaskForm(instance=task) + return render(request, 'core/task_edit.html', {'form': form, 'task': task}) + +def task_delete(request, pk): + task = get_object_or_404(Task, pk=pk) + task.delete() + return redirect('index') + +def task_set_status_completato(request, pk): + task = get_object_or_404(Task, pk=pk) + task.status = 'COMPLETATO' + task.save() + return redirect('index') + +def task_set_status_in_sospeso(request, pk): + task = get_object_or_404(Task, pk=pk) + task.status = 'IN_SOSPESO' + task.save() + return redirect('index') + +def task_set_status_da_fare(request, pk): + task = get_object_or_404(Task, pk=pk) + task.status = 'DA_FARE' + task.save() + return redirect('index') diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..b2f94df 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,68 @@ -/* Custom styles for the application */ body { - font-family: system-ui, -apple-system, sans-serif; + font-family: 'Lato', sans-serif; + background-color: #FDFEFE; + color: #5D6D7E; } + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; + color: #7D9D9C; +} + +.btn-primary { + background-color: #F5B7B1; + border-color: #F5B7B1; + color: #FFFFFF; + font-weight: 700; +} + +.btn-primary:hover { + background-color: #f2a299; + border-color: #f2a299; +} + +.hero { + background: linear-gradient(135deg, #7D9D9C, #A7C7E7); + color: white; + padding: 4rem 2rem; + border-radius: 0 0 30px 30px; +} + +.hero h1 { + color: white; +} + +.card { + border: none; + border-radius: 15px; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); + background-color: #FFFFFF; +} + +.card-title { + color: #7D9D9C; +} + +.task-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #E5E0D8; +} + +.task-item:last-child { + border-bottom: none; +} + +.fab-container { + position: fixed; + bottom: 30px; + right: 30px; + z-index: 100; +} + +#assistant-button { + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..b2f94df 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,68 @@ - -: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; - 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; + font-family: 'Lato', sans-serif; + background-color: #FDFEFE; + color: #5D6D7E; } + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; + color: #7D9D9C; +} + +.btn-primary { + background-color: #F5B7B1; + border-color: #F5B7B1; + color: #FFFFFF; + font-weight: 700; +} + +.btn-primary:hover { + background-color: #f2a299; + border-color: #f2a299; +} + +.hero { + background: linear-gradient(135deg, #7D9D9C, #A7C7E7); + color: white; + padding: 4rem 2rem; + border-radius: 0 0 30px 30px; +} + +.hero h1 { + color: white; +} + +.card { + border: none; + border-radius: 15px; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); + background-color: #FFFFFF; +} + +.card-title { + color: #7D9D9C; +} + +.task-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #E5E0D8; +} + +.task-item:last-child { + border-bottom: none; +} + +.fab-container { + position: fixed; + bottom: 30px; + right: 30px; + z-index: 100; +} + +#assistant-button { + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} \ No newline at end of file