diff --git a/diario/admin.py b/diario/admin.py index 5a48ba1..b0169da 100644 --- a/diario/admin.py +++ b/diario/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento +from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento, Tag, Appuntamento class AggiornamentoInline(admin.TabularInline): @@ -31,9 +31,9 @@ class DocumentoObiettivoInline(admin.TabularInline): @admin.register(Conversazione) class ConversazioneAdmin(admin.ModelAdmin): list_display = ('titolo', 'data', 'registrato_da') - list_filter = ('data', 'registrato_da') + list_filter = ('data', 'registrato_da', 'tags') search_fields = ('titolo', 'contenuto') - filter_horizontal = ('partecipanti',) + filter_horizontal = ('partecipanti', 'tags') readonly_fields = ('data',) inlines = [CommentoConversazioneInline, DocumentoInline] @@ -68,3 +68,23 @@ class DocumentoAdmin(admin.ModelAdmin): search_fields = ('titolo', 'descrizione') readonly_fields = ('data_caricamento',) + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + list_display = ('nome', 'colore') + search_fields = ('nome',) + + +class AppuntamentoPartecipantiInline(admin.TabularInline): + model = Appuntamento.partecipanti.through + extra = 0 + + +@admin.register(Appuntamento) +class AppuntamentoAdmin(admin.ModelAdmin): + list_display = ('titolo', 'data_ora', 'luogo', 'creato_da', 'conversazione') + list_filter = ('data_ora', 'creato_da') + search_fields = ('titolo', 'note', 'luogo') + filter_horizontal = ('partecipanti',) + readonly_fields = ('data_creazione',) + diff --git a/diario/forms.py b/diario/forms.py index 92496e7..67a3677 100644 --- a/diario/forms.py +++ b/diario/forms.py @@ -1,7 +1,7 @@ from django import forms from django.contrib.auth.models import User from django.utils import timezone -from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento +from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento, Tag, Appuntamento class ConversazioneForm(forms.ModelForm): @@ -20,10 +20,16 @@ class ConversazioneForm(forms.ModelForm): required=False, label='Partecipanti', ) + tags = forms.ModelMultipleChoiceField( + queryset=Tag.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label='Tag progetto', + ) class Meta: model = Conversazione - fields = ['titolo', 'data', 'contenuto', 'partecipanti'] + fields = ['titolo', 'data', 'contenuto', 'partecipanti', 'tags'] widgets = { 'titolo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Titolo della conversazione'}), 'contenuto': forms.Textarea(attrs={'class': 'form-control', 'rows': 6, 'placeholder': 'Descrivi cosa è stato discusso...'}), @@ -94,3 +100,29 @@ class DocumentoForm(forms.ModelForm): 'conversazione': 'Collegato a conversazione (opzionale)', 'obiettivo': 'Collegato a obiettivo (opzionale)', } + + +class AppuntamentoForm(forms.ModelForm): + data_ora = forms.DateTimeField( + label='Data e ora', + widget=forms.DateTimeInput( + attrs={'class': 'form-control', 'type': 'datetime-local'}, + format='%Y-%m-%dT%H:%M', + ), + input_formats=['%Y-%m-%dT%H:%M'], + ) + partecipanti = forms.ModelMultipleChoiceField( + queryset=User.objects.filter(is_active=True).order_by('first_name', 'username'), + widget=forms.CheckboxSelectMultiple, + required=False, + label='Partecipanti', + ) + + class Meta: + model = Appuntamento + fields = ['titolo', 'data_ora', 'luogo', 'note', 'partecipanti'] + widgets = { + 'titolo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Titolo appuntamento/riunione'}), + 'luogo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Luogo (opzionale)'}), + 'note': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Note (opzionale)'}), + } diff --git a/diario/migrations/0006_tag_appuntamento.py b/diario/migrations/0006_tag_appuntamento.py new file mode 100644 index 0000000..bc36989 --- /dev/null +++ b/diario/migrations/0006_tag_appuntamento.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.12 on 2026-04-07 14:27 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('diario', '0005_commento_conversazione'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nome', models.CharField(max_length=50, unique=True)), + ('colore', models.CharField(default='#4361ee', max_length=7)), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tag', + 'ordering': ['nome'], + }, + ), + migrations.AlterField( + model_name='conversazione', + name='data', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.CreateModel( + name='Appuntamento', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('titolo', models.CharField(max_length=200)), + ('data_ora', models.DateTimeField()), + ('luogo', models.CharField(blank=True, max_length=200)), + ('note', models.TextField(blank=True)), + ('data_creazione', models.DateTimeField(auto_now_add=True)), + ('conversazione', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appuntamenti', to='diario.conversazione')), + ('creato_da', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appuntamenti_creati', to=settings.AUTH_USER_MODEL)), + ('partecipanti', models.ManyToManyField(blank=True, related_name='appuntamenti', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Appuntamento', + 'verbose_name_plural': 'Appuntamenti', + 'ordering': ['data_ora'], + }, + ), + migrations.AddField( + model_name='conversazione', + name='tags', + field=models.ManyToManyField(blank=True, related_name='conversazioni', to='diario.tag'), + ), + ] diff --git a/diario/models.py b/diario/models.py index 8e731ca..7f96784 100644 --- a/diario/models.py +++ b/diario/models.py @@ -11,14 +11,28 @@ def validate_file_size(value): raise ValidationError('Il file non può superare i 10 MB.') +class Tag(models.Model): + nome = models.CharField(max_length=50, unique=True) + colore = models.CharField(max_length=7, default='#4361ee') # colore hex + + class Meta: + ordering = ['nome'] + verbose_name = 'Tag' + verbose_name_plural = 'Tag' + + def __str__(self): + return self.nome + + class Conversazione(models.Model): titolo = models.CharField(max_length=200) - data = models.DateTimeField() + data = models.DateTimeField(default=timezone.now) partecipanti = models.ManyToManyField(User, related_name='conversazioni_partecipate', blank=True) contenuto = models.TextField() registrato_da = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name='conversazioni_registrate' ) + tags = models.ManyToManyField(Tag, blank=True, related_name='conversazioni') class Meta: ordering = ['-data'] @@ -137,3 +151,30 @@ class Documento(models.Model): import os return os.path.basename(self.file.name) + +class Appuntamento(models.Model): + titolo = models.CharField(max_length=200) + data_ora = models.DateTimeField() + luogo = models.CharField(max_length=200, blank=True) + note = models.TextField(blank=True) + conversazione = models.ForeignKey( + Conversazione, on_delete=models.SET_NULL, null=True, blank=True, related_name='appuntamenti' + ) + partecipanti = models.ManyToManyField(User, blank=True, related_name='appuntamenti') + creato_da = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, related_name='appuntamenti_creati' + ) + data_creazione = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['data_ora'] + verbose_name = 'Appuntamento' + verbose_name_plural = 'Appuntamenti' + + def __str__(self): + return f"{self.titolo} ({self.data_ora.strftime('%d/%m/%Y %H:%M')})" + + @property + def is_passato(self): + return self.data_ora < timezone.now() + diff --git a/diario/templatetags/custom_filters.py b/diario/templatetags/custom_filters.py index f98d105..a92a4c8 100644 --- a/diario/templatetags/custom_filters.py +++ b/diario/templatetags/custom_filters.py @@ -1,4 +1,9 @@ +import re from django import template +from django.contrib.auth.models import User +from django.urls import reverse +from django.utils.safestring import mark_safe +from django.utils.html import escape register = template.Library() @@ -10,3 +15,23 @@ def abs_val(value): return abs(int(value)) except (ValueError, TypeError): return value + + +@register.filter(needs_autoescape=True) +def render_mentions(text, autoescape=True): + """Converte @username in link al profilo della persona.""" + if autoescape: + text = escape(text) + + def replace_mention(match): + username = match.group(1) + try: + user = User.objects.get(username=username, is_active=True) + url = reverse('persona_dettaglio', args=[user.pk]) + nome = user.get_full_name() or user.username + return f'@{escape(nome)}' + except User.DoesNotExist: + return match.group(0) + + result = re.sub(r'@(\w[\w.]+)', replace_mention, text) + return mark_safe(result) diff --git a/diario/urls.py b/diario/urls.py index 5b20c06..4b9dc4b 100644 --- a/diario/urls.py +++ b/diario/urls.py @@ -34,10 +34,20 @@ urlpatterns = [ path('documenti//', views.documento_dettaglio, name='documento_dettaglio'), path('documenti//elimina/', views.documento_elimina, name='documento_elimina'), + # Appuntamenti + path('appuntamenti/', views.appuntamenti_lista, name='appuntamenti_lista'), + path('appuntamenti/nuovo/', views.appuntamento_nuovo, name='appuntamento_nuovo'), + path('appuntamenti//', views.appuntamento_dettaglio, name='appuntamento_dettaglio'), + path('appuntamenti//modifica/', views.appuntamento_modifica, name='appuntamento_modifica'), + path('appuntamenti//elimina/', views.appuntamento_elimina, name='appuntamento_elimina'), + # Ricerca path('ricerca/', views.ricerca, name='ricerca'), # Persone path('persone/', views.persone_lista, name='persone_lista'), path('persone//', views.persona_dettaglio, name='persona_dettaglio'), + + # API + path('api/utenti/', views.api_utenti, name='api_utenti'), ] diff --git a/diario/views.py b/diario/views.py index a6c58f4..59d01d5 100644 --- a/diario/views.py +++ b/diario/views.py @@ -9,8 +9,8 @@ from django.utils import timezone from itertools import chain from datetime import timedelta -from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento -from .forms import ConversazioneForm, ObiettivoForm, AggiornamentoObiettivoForm, CommentoConversazioneForm, DocumentoForm +from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento, Tag, Appuntamento +from .forms import ConversazioneForm, ObiettivoForm, AggiornamentoObiettivoForm, CommentoConversazioneForm, DocumentoForm, AppuntamentoForm def _can_edit(user, obj): @@ -51,11 +51,17 @@ def dashboard(request): data_scadenza__lt=oggi, ).exclude(stato='completato').order_by('-data_scadenza')[:5] + # Prossimi appuntamenti + prossimi_appuntamenti = Appuntamento.objects.filter( + data_ora__gte=timezone.now(), + ).select_related('creato_da', 'conversazione').order_by('data_ora')[:5] + return render(request, 'diario/dashboard.html', { 'eventi': eventi, 'obiettivi_aperti': obiettivi_aperti, 'scadenze_prossime': scadenze_prossime, 'scaduti': scaduti, + 'prossimi_appuntamenti': prossimi_appuntamenti, 'oggi': oggi, }) @@ -66,7 +72,7 @@ def dashboard(request): def agenda(request): oggi = timezone.now().date() - # Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro + # Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro, appuntamenti scadenze_future = Obiettivo.objects.filter( data_scadenza__gte=oggi, ).exclude(stato='completato').order_by('data_scadenza') @@ -75,11 +81,16 @@ def agenda(request): data__date__gte=oggi, ).select_related('registrato_da').order_by('data') + appuntamenti_futuri = Appuntamento.objects.filter( + data_ora__date__gte=oggi, + ).select_related('creato_da', 'conversazione').order_by('data_ora') + # Eventi futuri unificati eventi_futuri = sorted( chain( [{'tipo': 'scadenza', 'data': o.data_scadenza, 'obj': o} for o in scadenze_future], [{'tipo': 'conversazione', 'data': c.data.date(), 'obj': c} for c in conversazioni_future], + [{'tipo': 'appuntamento', 'data': a.data_ora.date(), 'obj': a} for a in appuntamenti_futuri], ), key=lambda x: x['data'], ) @@ -96,10 +107,16 @@ def agenda(request): data_scadenza__lt=oggi, ).order_by('-data_scadenza') + appuntamenti_passati = Appuntamento.objects.filter( + data_ora__date__gte=data_inizio, + data_ora__date__lt=oggi, + ).select_related('creato_da', 'conversazione').order_by('-data_ora') + eventi_passati = sorted( chain( [{'tipo': 'scadenza', 'data': o.data_scadenza, 'obj': o, 'scaduto': o.stato != 'completato'} for o in scadenze_passate], [{'tipo': 'conversazione', 'data': c.data.date(), 'obj': c, 'scaduto': False} for c in conversazioni_passate], + [{'tipo': 'appuntamento', 'data': a.data_ora.date(), 'obj': a, 'scaduto': False} for a in appuntamenti_passati], ), key=lambda x: x['data'], reverse=True, @@ -141,6 +158,7 @@ def conversazione_dettaglio(request, pk): conv = get_object_or_404(Conversazione, pk=pk) commenti = conv.commenti.select_related('autore').order_by('-data') documenti = conv.documenti.select_related('caricato_da').all() + appuntamenti = conv.appuntamenti.select_related('creato_da').all() if request.method == 'POST': comment_form = CommentoConversazioneForm(request.POST) @@ -159,6 +177,7 @@ def conversazione_dettaglio(request, pk): 'commenti': commenti, 'comment_form': comment_form, 'documenti': documenti, + 'appuntamenti': appuntamenti, 'can_edit': _can_edit(request.user, conv), }) @@ -515,3 +534,114 @@ def persona_dettaglio(request, pk): 'documenti': documenti, }) + +# ── Appuntamenti ─────────────────────────────────────────────────────────────── + +@login_required +def appuntamenti_lista(request): + oggi = timezone.now() + futuri = Appuntamento.objects.filter(data_ora__gte=oggi).select_related('creato_da', 'conversazione').order_by('data_ora') + passati = Appuntamento.objects.filter(data_ora__lt=oggi).select_related('creato_da', 'conversazione').order_by('-data_ora')[:30] + return render(request, 'diario/appuntamenti/lista.html', { + 'futuri': futuri, + 'passati': passati, + }) + + +@login_required +def appuntamento_nuovo(request): + conversazione_pk = request.GET.get('conversazione') + initial = {} + if conversazione_pk: + conv = get_object_or_404(Conversazione, pk=conversazione_pk) + initial['titolo'] = f"Riunione: {conv.titolo}" + initial['partecipanti'] = conv.partecipanti.all() + + if request.method == 'POST': + form = AppuntamentoForm(request.POST) + if form.is_valid(): + app = form.save(commit=False) + app.creato_da = request.user + if conversazione_pk: + app.conversazione_id = conversazione_pk + app.save() + form.save_m2m() + messages.success(request, 'Appuntamento creato.') + if conversazione_pk: + return redirect('conversazione_dettaglio', pk=conversazione_pk) + return redirect('appuntamento_dettaglio', pk=app.pk) + else: + form = AppuntamentoForm(initial=initial) + return render(request, 'diario/appuntamenti/form.html', { + 'form': form, + 'titolo_pagina': 'Nuovo appuntamento', + 'conversazione_pk': conversazione_pk, + }) + + +@login_required +def appuntamento_dettaglio(request, pk): + app = get_object_or_404(Appuntamento.objects.select_related('creato_da', 'conversazione'), pk=pk) + return render(request, 'diario/appuntamenti/dettaglio.html', { + 'app': app, + 'can_edit': _can_edit(request.user, app), + }) + + +@login_required +def appuntamento_modifica(request, pk): + app = get_object_or_404(Appuntamento, pk=pk) + if not _can_edit(request.user, app): + return HttpResponseForbidden('Non hai i permessi per modificare questo appuntamento.') + if request.method == 'POST': + form = AppuntamentoForm(request.POST, instance=app) + if form.is_valid(): + form.save() + messages.success(request, 'Appuntamento aggiornato.') + return redirect('appuntamento_dettaglio', pk=app.pk) + else: + form = AppuntamentoForm(instance=app) + return render(request, 'diario/appuntamenti/form.html', { + 'form': form, + 'titolo_pagina': 'Modifica appuntamento', + 'app': app, + }) + + +@login_required +def appuntamento_elimina(request, pk): + app = get_object_or_404(Appuntamento, pk=pk) + if not _can_edit(request.user, app): + return HttpResponseForbidden('Non hai i permessi per eliminare questo appuntamento.') + if request.method == 'POST': + app.delete() + messages.success(request, 'Appuntamento eliminato.') + return redirect('appuntamenti_lista') + return render(request, 'diario/conferma_elimina.html', { + 'oggetto': app, + 'tipo': 'appuntamento', + 'cancel_url': 'appuntamento_dettaglio', + }) + + +# ── API: utenti per @menzioni ────────────────────────────────────────────────── + +@login_required +def api_utenti(request): + """Restituisce lista utenti per autocomplete @menzioni.""" + q = request.GET.get('q', '').strip() + utenti = User.objects.filter(is_active=True) + if q: + utenti = utenti.filter( + Q(username__icontains=q) | Q(first_name__icontains=q) | Q(last_name__icontains=q) + ) + data = [ + { + 'username': u.username, + 'nome': u.get_full_name() or u.username, + 'pk': u.pk, + } + for u in utenti.order_by('first_name', 'username')[:15] + ] + return JsonResponse(data, safe=False) + diff --git a/static/css/style.css b/static/css/style.css index 3ac75aa..555fc70 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -327,3 +327,74 @@ textarea.form-control:focus, input.form-control:focus { border-color: var(--acce .card:hover .dropdown .btn-icon { opacity: 1; } + +/* ── @Mentions ── */ +.mention-link { + color: var(--accent); + font-weight: 600; + text-decoration: none; + background: var(--accent-soft); + padding: .1em .35em; + border-radius: 4px; +} +.mention-link:hover { + text-decoration: underline; + color: var(--accent-hover); +} +.mention-dropdown { + position: absolute; + bottom: 100%; + left: 0; + right: 0; + margin-bottom: 4px; + background: #fff; + border-radius: 10px; + box-shadow: 0 4px 16px rgba(0,0,0,.15); + z-index: 1050; + max-height: 220px; + overflow-y: auto; + display: none; +} +.mention-dropdown.show { display: block; } +.mention-item { + padding: .5rem .75rem; + cursor: pointer; + display: flex; + align-items: center; + gap: .5rem; + font-size: .85rem; + transition: background .1s; +} +.mention-item:hover, +.mention-item.active { + background: var(--accent-soft); +} +.mention-item small { + margin-left: auto; +} + +/* ── Tag pills ── */ +.tag-pill { + font-size: .68rem; + font-weight: 600; + padding: .2em .55em; + border-radius: 20px; + display: inline-flex; + align-items: center; + gap: .25rem; + border: 1.5px solid; +} +.tag-dot { + width: 7px; + height: 7px; + border-radius: 50%; + display: inline-block; +} + +/* ── Appuntamento ── */ +.agenda-icon-appuntamento { + background: #fef3c7; color: #d97706; +} +.appuntamento-card { + border-left: 3px solid #d97706 !important; +} diff --git a/static/js/app.js b/static/js/app.js index 991bd60..e25264b 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -148,6 +148,102 @@ function initApp() { convertMessagesToToasts(); initSliders(); initLiveSearch(); + initMentions(); +} + +// ── @Mentions autocomplete ── +function initMentions() { + document.querySelectorAll('textarea.form-control').forEach(textarea => { + if (textarea._mentionInit) return; + textarea._mentionInit = true; + + let dropdown = textarea.parentElement.querySelector('.mention-dropdown'); + if (!dropdown) { + dropdown = document.createElement('div'); + dropdown.className = 'mention-dropdown'; + textarea.parentElement.style.position = 'relative'; + textarea.parentElement.appendChild(dropdown); + } + + let mentionStart = -1; + let timer = null; + + textarea.addEventListener('input', () => { + const pos = textarea.selectionStart; + const text = textarea.value.substring(0, pos); + const atMatch = text.match(/@(\w[\w.]*)$/); + + if (!atMatch) { + dropdown.classList.remove('show'); + mentionStart = -1; + return; + } + + mentionStart = pos - atMatch[0].length; + const query = atMatch[1]; + + clearTimeout(timer); + timer = setTimeout(() => { + fetch('/api/utenti/?q=' + encodeURIComponent(query)) + .then(r => r.json()) + .then(users => { + if (!users.length) { + dropdown.classList.remove('show'); + return; + } + dropdown.innerHTML = users.map(u => + `
+ ${u.username.substring(0,2).toUpperCase()} + ${u.nome} + @${u.username} +
` + ).join(''); + dropdown.classList.add('show'); + }); + }, 200); + }); + + dropdown.addEventListener('click', (e) => { + const item = e.target.closest('.mention-item'); + if (!item) return; + const username = item.dataset.username; + const before = textarea.value.substring(0, mentionStart); + const after = textarea.value.substring(textarea.selectionStart); + textarea.value = before + '@' + username + ' ' + after; + textarea.focus(); + const newPos = mentionStart + username.length + 2; + textarea.setSelectionRange(newPos, newPos); + dropdown.classList.remove('show'); + }); + + textarea.addEventListener('blur', () => { + setTimeout(() => dropdown.classList.remove('show'), 200); + }); + + textarea.addEventListener('keydown', (e) => { + if (!dropdown.classList.contains('show')) return; + const items = dropdown.querySelectorAll('.mention-item'); + const active = dropdown.querySelector('.mention-item.active'); + let idx = Array.from(items).indexOf(active); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (active) active.classList.remove('active'); + idx = (idx + 1) % items.length; + items[idx].classList.add('active'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (active) active.classList.remove('active'); + idx = idx <= 0 ? items.length - 1 : idx - 1; + items[idx].classList.add('active'); + } else if (e.key === 'Enter' && active) { + e.preventDefault(); + active.click(); + } else if (e.key === 'Escape') { + dropdown.classList.remove('show'); + } + }); + }); } // Run on initial page load diff --git a/templates/diario/agenda.html b/templates/diario/agenda.html index acd86af..84a18f3 100644 --- a/templates/diario/agenda.html +++ b/templates/diario/agenda.html @@ -12,6 +12,9 @@ Panoramica di scadenze e appuntamenti
+ + Appuntamento + Obiettivo @@ -42,6 +45,10 @@
+ {% elif ev.tipo == 'appuntamento' %} +
+ +
{% else %}
@@ -69,6 +76,14 @@ {% endif %} {% endwith %}
+ {% elif ev.tipo == 'appuntamento' %} + + {{ ev.obj.titolo }} + +
+ {{ ev.obj.data_ora|date:"H:i" }} + {% if ev.obj.luogo %}{{ ev.obj.luogo }}{% endif %} +
{% else %} {{ ev.obj.titolo }} @@ -126,6 +141,10 @@
+ {% elif ev.tipo == 'appuntamento' %} +
+ +
{% else %}
@@ -142,6 +161,11 @@
{{ ev.obj.get_stato_display }}
+ {% elif ev.tipo == 'appuntamento' %} +
+ {{ ev.obj.titolo }} + + {{ ev.obj.data_ora|date:"H:i" }}{% if ev.obj.luogo %} · {{ ev.obj.luogo }}{% endif %} {% else %} {{ ev.obj.titolo }} diff --git a/templates/diario/appuntamenti/dettaglio.html b/templates/diario/appuntamenti/dettaglio.html new file mode 100644 index 0000000..b6b8563 --- /dev/null +++ b/templates/diario/appuntamenti/dettaglio.html @@ -0,0 +1,80 @@ +{% extends "diario/base.html" %} +{% block title %}{{ app.titolo }}{% endblock %} + +{% block content %} +
+
+ +
+ + + +
+ {{ app.titolo }} +
+ {% if can_edit %} + + Modifica + + + Elimina + + {% endif %} +
+ +
+
+
+ Data e ora +
+ + {{ app.data_ora|date:"d/m/Y \a\l\l\e H:i" }} +
+ {% if app.is_passato %} + Passato + {% endif %} +
+ {% if app.luogo %} +
+ Luogo +
{{ app.luogo }}
+
+ {% endif %} +
+ + {% if app.note %} +
+ Note +
{{ app.note }}
+ {% endif %} + + {% if app.conversazione %} +
+ Conversazione collegata + + {{ app.conversazione.titolo }} + + {% endif %} + + {% if app.partecipanti.all %} +
+ Partecipanti +
+ {% for p in app.partecipanti.all %} +
+ {{ p.username|slice:":2"|upper }} + {{ p.get_full_name|default:p.username }} +
+ {% endfor %} +
+ {% endif %} + +
+ + Creato da {{ app.creato_da.get_full_name|default:app.creato_da.username }} il {{ app.data_creazione|date:"d/m/Y H:i" }} + +
+ +
+
+{% endblock %} diff --git a/templates/diario/appuntamenti/form.html b/templates/diario/appuntamenti/form.html new file mode 100644 index 0000000..4decee4 --- /dev/null +++ b/templates/diario/appuntamenti/form.html @@ -0,0 +1,63 @@ +{% extends "diario/base.html" %} +{% block title %}{{ titolo_pagina }}{% endblock %} + +{% block content %} +
+
+
+ + + +

{{ titolo_pagina }}

+
+ +
+
+ {% csrf_token %} +
+ + {{ form.titolo }} +
+
+ + {{ form.data_ora }} +
+
+ + {{ form.luogo }} +
+
+ + {{ form.note }} +
+
+ +
+ {% for checkbox in form.partecipanti %} +
+
+ {{ checkbox.tag }} + +
+
+ {% endfor %} +
+
+ {% if conversazione_pk %} + + {% endif %} +
+ + {% if app %} + Annulla + {% else %} + Annulla + {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/templates/diario/appuntamenti/lista.html b/templates/diario/appuntamenti/lista.html new file mode 100644 index 0000000..1df167d --- /dev/null +++ b/templates/diario/appuntamenti/lista.html @@ -0,0 +1,69 @@ +{% extends "diario/base.html" %} +{% block title %}Appuntamenti{% endblock %} + +{% block content %} +
+

Appuntamenti e Riunioni

+ + Nuovo appuntamento + +
+ +{% if futuri %} +

Prossimi

+{% for app in futuri %} +
+
+
+ + {{ app.titolo }} + +
+ {{ app.data_ora|date:"d/m/Y H:i" }} + {% if app.luogo %} + {{ app.luogo }} + {% endif %} + {% if app.conversazione %} + + + {{ app.conversazione.titolo|truncatewords:5 }} + + {% endif %} + {% if app.partecipanti.count > 0 %} + {{ app.partecipanti.count }} + {% endif %} +
+
+
+
+{% endfor %} +{% endif %} + +{% if passati %} +

Passati

+{% for app in passati %} +
+
+
+ + {{ app.titolo }} + +
+ {{ app.data_ora|date:"d/m/Y H:i" }} + {% if app.luogo %} + {{ app.luogo }} + {% endif %} +
+
+
+
+{% endfor %} +{% endif %} + +{% if not futuri and not passati %} +
+ +

Nessun appuntamento registrato.

+
+{% endif %} +{% endblock %} diff --git a/templates/diario/base.html b/templates/diario/base.html index 54177f9..58c99f8 100644 --- a/templates/diario/base.html +++ b/templates/diario/base.html @@ -41,6 +41,11 @@ Agenda +
-

{{ c.testo }}

+

{{ c.testo|render_mentions }}

{% empty %}

Nessun commento ancora. Sii il primo!

diff --git a/templates/diario/conversazioni/form.html b/templates/diario/conversazioni/form.html index 7371ba2..d3d0b9f 100644 --- a/templates/diario/conversazioni/form.html +++ b/templates/diario/conversazioni/form.html @@ -42,6 +42,22 @@ {% endfor %} + {% if form.tags.field.queryset.count > 0 %} +
+ +
+ {% for checkbox in form.tags %} +
+ {{ checkbox.tag }} + +
+ {% endfor %} +
+
Seleziona i tag progetto da associare.
+
+ {% endif %}
{% if conv %} diff --git a/templates/diario/conversazioni/lista.html b/templates/diario/conversazioni/lista.html index b2b0f1b..be1676b 100644 --- a/templates/diario/conversazioni/lista.html +++ b/templates/diario/conversazioni/lista.html @@ -27,6 +27,16 @@ {% endif %}

{{ conv.contenuto|truncatewords:30 }}

+ {% if conv.tags.all %} +
+ {% for tag in conv.tags.all %} + + + {{ tag.nome }} + + {% endfor %} +
+ {% endif %} {% if user == conv.registrato_da or user.is_superuser %}
diff --git a/templates/diario/dashboard.html b/templates/diario/dashboard.html index 88582c2..98e8857 100644 --- a/templates/diario/dashboard.html +++ b/templates/diario/dashboard.html @@ -230,6 +230,32 @@ {% endif %} + + {% if prossimi_appuntamenti %} +
+ +
+ {% for app in prossimi_appuntamenti %} +
+
+
{{ app.data_ora|date:"d" }}
+
{{ app.data_ora|date:"M" }}
+
+
+ + {{ app.titolo|truncatewords:6 }} + + {{ app.data_ora|date:"H:i" }}{% if app.luogo %} · {{ app.luogo }}{% endif %} +
+
+ {% endfor %} +
+
+ {% endif %} + diff --git a/templates/diario/obiettivi/dettaglio.html b/templates/diario/obiettivi/dettaglio.html index 58d4a0b..58a5045 100644 --- a/templates/diario/obiettivi/dettaglio.html +++ b/templates/diario/obiettivi/dettaglio.html @@ -162,7 +162,7 @@ {% endif %} -

{{ agg.testo }}

+

{{ agg.testo|render_mentions }}

{% empty %}

Nessun aggiornamento ancora.