from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.contrib import messages from django.http import JsonResponse, HttpResponseForbidden from django.views.decorators.http import require_POST from django.db.models import Q, Count 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 def _can_edit(user, obj): """Autore o superuser possono modificare/eliminare.""" owner = getattr(obj, 'registrato_da', None) or getattr(obj, 'creato_da', None) or getattr(obj, 'caricato_da', None) or getattr(obj, 'autore', None) return user == owner or user.is_superuser # ── Dashboard ────────────────────────────────────────────────────────────────── @login_required def dashboard(request): conversazioni = Conversazione.objects.select_related('registrato_da').order_by('-data')[:20] aggiornamenti = AggiornamentoObiettivo.objects.select_related('autore', 'obiettivo').order_by('-data')[:20] documenti_recenti = Documento.objects.select_related('caricato_da').order_by('-data_caricamento')[:10] eventi = sorted( chain( [{'tipo': 'conversazione', 'data': c.data, 'obj': c} for c in conversazioni], [{'tipo': 'aggiornamento', 'data': a.data, 'obj': a} for a in aggiornamenti], [{'tipo': 'documento', 'data': d.data_caricamento, 'obj': d} for d in documenti_recenti], ), key=lambda x: x['data'], reverse=True, )[:30] obiettivi_aperti = Obiettivo.objects.exclude(stato='completato').order_by('-data_creazione')[:5] # Agenda: prossime scadenze obiettivi (prossimi 30 giorni) oggi = timezone.now().date() scadenze_prossime = Obiettivo.objects.filter( data_scadenza__gte=oggi, data_scadenza__lte=oggi + timedelta(days=30), ).exclude(stato='completato').order_by('data_scadenza')[:8] # Scadenze passate non completate scaduti = Obiettivo.objects.filter( data_scadenza__lt=oggi, ).exclude(stato='completato').order_by('-data_scadenza')[:5] return render(request, 'diario/dashboard.html', { 'eventi': eventi, 'obiettivi_aperti': obiettivi_aperti, 'scadenze_prossime': scadenze_prossime, 'scaduti': scaduti, 'oggi': oggi, }) # ── Agenda ───────────────────────────────────────────────────────────────────── @login_required def agenda(request): oggi = timezone.now().date() # Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro scadenze_future = Obiettivo.objects.filter( data_scadenza__gte=oggi, ).exclude(stato='completato').order_by('data_scadenza') conversazioni_future = Conversazione.objects.filter( data__date__gte=oggi, ).select_related('registrato_da').order_by('data') # 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], ), key=lambda x: x['data'], ) # Eventi passati (ultimi 30 giorni) data_inizio = oggi - timedelta(days=30) conversazioni_passate = Conversazione.objects.filter( data__date__gte=data_inizio, data__date__lt=oggi, ).select_related('registrato_da').order_by('-data') scadenze_passate = Obiettivo.objects.filter( data_scadenza__gte=data_inizio, data_scadenza__lt=oggi, ).order_by('-data_scadenza') 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], ), key=lambda x: x['data'], reverse=True, ) return render(request, 'diario/agenda.html', { 'eventi_futuri': eventi_futuri, 'eventi_passati': eventi_passati, 'oggi': oggi, }) # ── Conversazioni ────────────────────────────────────────────────────────────── @login_required def conversazioni_lista(request): qs = Conversazione.objects.select_related('registrato_da').order_by('-data') return render(request, 'diario/conversazioni/lista.html', {'conversazioni': qs}) @login_required def conversazione_nuova(request): if request.method == 'POST': form = ConversazioneForm(request.POST) if form.is_valid(): conv = form.save(commit=False) conv.registrato_da = request.user conv.save() form.save_m2m() messages.success(request, 'Conversazione registrata.') return redirect('conversazione_dettaglio', pk=conv.pk) else: form = ConversazioneForm() return render(request, 'diario/conversazioni/form.html', {'form': form, 'titolo_pagina': 'Nuova conversazione'}) @login_required 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() if request.method == 'POST': comment_form = CommentoConversazioneForm(request.POST) if comment_form.is_valid(): c = comment_form.save(commit=False) c.conversazione = conv c.autore = request.user c.save() messages.success(request, 'Commento aggiunto.') return redirect('conversazione_dettaglio', pk=conv.pk) else: comment_form = CommentoConversazioneForm() return render(request, 'diario/conversazioni/dettaglio.html', { 'conv': conv, 'commenti': commenti, 'comment_form': comment_form, 'documenti': documenti, 'can_edit': _can_edit(request.user, conv), }) @login_required def conversazione_modifica(request, pk): conv = get_object_or_404(Conversazione, pk=pk) if not _can_edit(request.user, conv): return HttpResponseForbidden('Non hai i permessi per modificare questa conversazione.') if request.method == 'POST': form = ConversazioneForm(request.POST, instance=conv) if form.is_valid(): form.save() messages.success(request, 'Conversazione aggiornata.') return redirect('conversazione_dettaglio', pk=conv.pk) else: form = ConversazioneForm(instance=conv) return render(request, 'diario/conversazioni/form.html', {'form': form, 'titolo_pagina': 'Modifica conversazione', 'conv': conv}) @login_required def conversazione_elimina(request, pk): conv = get_object_or_404(Conversazione, pk=pk) if not _can_edit(request.user, conv): return HttpResponseForbidden('Non hai i permessi per eliminare questa conversazione.') if request.method == 'POST': conv.delete() messages.success(request, 'Conversazione eliminata.') return redirect('conversazioni_lista') return render(request, 'diario/conferma_elimina.html', { 'oggetto': conv, 'tipo': 'conversazione', 'cancel_url': 'conversazione_dettaglio', }) # ── Commenti conversazione — modifica/elimina ───────────────────────────────── @login_required def commento_modifica(request, pk): commento = get_object_or_404(CommentoConversazione, pk=pk) if not _can_edit(request.user, commento): return HttpResponseForbidden('Non puoi modificare questo commento.') if request.method == 'POST': form = CommentoConversazioneForm(request.POST, instance=commento) if form.is_valid(): form.save() messages.success(request, 'Commento aggiornato.') return redirect('conversazione_dettaglio', pk=commento.conversazione.pk) else: form = CommentoConversazioneForm(instance=commento) return render(request, 'diario/commento_modifica.html', { 'form': form, 'commento': commento, 'tipo': 'commento', 'back_url': redirect('conversazione_dettaglio', pk=commento.conversazione.pk).url, }) @login_required def commento_elimina(request, pk): commento = get_object_or_404(CommentoConversazione, pk=pk) if not _can_edit(request.user, commento): return HttpResponseForbidden('Non puoi eliminare questo commento.') conv_pk = commento.conversazione.pk if request.method == 'POST': commento.delete() messages.success(request, 'Commento eliminato.') return redirect('conversazione_dettaglio', pk=conv_pk) return render(request, 'diario/conferma_elimina.html', { 'oggetto': commento, 'tipo': 'commento', 'cancel_url': 'conversazione_dettaglio', 'cancel_pk': conv_pk, }) # ── Aggiornamenti obiettivo — modifica/elimina ──────────────────────────────── @login_required def aggiornamento_modifica(request, pk): agg = get_object_or_404(AggiornamentoObiettivo, pk=pk) if not _can_edit(request.user, agg): return HttpResponseForbidden('Non puoi modificare questo aggiornamento.') if request.method == 'POST': form = AggiornamentoObiettivoForm(request.POST, instance=agg) if form.is_valid(): form.save() messages.success(request, 'Aggiornamento modificato.') return redirect('obiettivo_dettaglio', pk=agg.obiettivo.pk) else: form = AggiornamentoObiettivoForm(instance=agg) return render(request, 'diario/commento_modifica.html', { 'form': form, 'commento': agg, 'tipo': 'aggiornamento', 'back_url': redirect('obiettivo_dettaglio', pk=agg.obiettivo.pk).url, }) @login_required def aggiornamento_elimina(request, pk): agg = get_object_or_404(AggiornamentoObiettivo, pk=pk) if not _can_edit(request.user, agg): return HttpResponseForbidden('Non puoi eliminare questo aggiornamento.') obj_pk = agg.obiettivo.pk if request.method == 'POST': agg.delete() messages.success(request, 'Aggiornamento eliminato.') return redirect('obiettivo_dettaglio', pk=obj_pk) return render(request, 'diario/conferma_elimina.html', { 'oggetto': agg, 'tipo': 'aggiornamento', 'cancel_url': 'obiettivo_dettaglio', 'cancel_pk': obj_pk, }) # ── Obiettivi ────────────────────────────────────────────────────────────────── @login_required def obiettivi_lista(request): filtro = request.GET.get('filtro', 'tutti') qs = Obiettivo.objects.prefetch_related('assegnato_a').select_related('creato_da') if filtro == 'collettivi': qs = qs.filter(tipo='collettivo') elif filtro == 'individuali': qs = qs.filter(tipo='individuale') elif filtro == 'miei': qs = qs.filter(assegnato_a=request.user) return render(request, 'diario/obiettivi/lista.html', {'obiettivi': qs, 'filtro': filtro}) @login_required def obiettivo_nuovo(request): if request.method == 'POST': form = ObiettivoForm(request.POST) if form.is_valid(): obj = form.save(commit=False) obj.creato_da = request.user obj.save() form.save_m2m() messages.success(request, 'Obiettivo creato.') return redirect('obiettivo_dettaglio', pk=obj.pk) else: form = ObiettivoForm() return render(request, 'diario/obiettivi/form.html', {'form': form, 'titolo_pagina': 'Nuovo obiettivo'}) @login_required def obiettivo_dettaglio(request, pk): obj = get_object_or_404(Obiettivo, pk=pk) aggiornamenti = obj.aggiornamenti.select_related('autore').order_by('-data') documenti = obj.documenti.select_related('caricato_da').all() if request.method == 'POST': agg_form = AggiornamentoObiettivoForm(request.POST) if agg_form.is_valid(): agg = agg_form.save(commit=False) agg.obiettivo = obj agg.autore = request.user agg.save() messages.success(request, 'Aggiornamento aggiunto.') return redirect('obiettivo_dettaglio', pk=obj.pk) else: agg_form = AggiornamentoObiettivoForm() return render(request, 'diario/obiettivi/dettaglio.html', { 'obj': obj, 'aggiornamenti': aggiornamenti, 'documenti': documenti, 'agg_form': agg_form, 'can_edit': _can_edit(request.user, obj), }) @login_required def obiettivo_modifica(request, pk): obj = get_object_or_404(Obiettivo, pk=pk) if not _can_edit(request.user, obj): return HttpResponseForbidden('Non hai i permessi per modificare questo obiettivo.') if request.method == 'POST': form = ObiettivoForm(request.POST, instance=obj) if form.is_valid(): form.save() messages.success(request, 'Obiettivo aggiornato.') return redirect('obiettivo_dettaglio', pk=obj.pk) else: form = ObiettivoForm(instance=obj) return render(request, 'diario/obiettivi/form.html', {'form': form, 'titolo_pagina': 'Modifica obiettivo', 'obj': obj}) @login_required def obiettivo_elimina(request, pk): obj = get_object_or_404(Obiettivo, pk=pk) if not _can_edit(request.user, obj): return HttpResponseForbidden('Non hai i permessi per eliminare questo obiettivo.') if request.method == 'POST': obj.delete() messages.success(request, 'Obiettivo eliminato.') return redirect('obiettivi_lista') return render(request, 'diario/conferma_elimina.html', { 'oggetto': obj, 'tipo': 'obiettivo', 'cancel_url': 'obiettivo_dettaglio', }) @login_required @require_POST def obiettivo_avanzamento_ajax(request, pk): obj = get_object_or_404(Obiettivo, pk=pk) try: valore = int(request.POST.get('avanzamento', 0)) if not 0 <= valore <= 100: return JsonResponse({'ok': False, 'error': 'Valore fuori range'}, status=400) obj.avanzamento = valore obj.save(update_fields=['avanzamento']) return JsonResponse({'ok': True, 'avanzamento': obj.avanzamento}) except (ValueError, TypeError): return JsonResponse({'ok': False, 'error': 'Valore non valido'}, status=400) # ── Documenti ────────────────────────────────────────────────────────────────── @login_required def documenti_lista(request): qs = Documento.objects.select_related('caricato_da', 'conversazione', 'obiettivo').order_by('-data_caricamento') return render(request, 'diario/documenti/lista.html', {'documenti': qs}) @login_required def documento_nuovo(request): conversazione_pk = request.GET.get('conversazione') obiettivo_pk = request.GET.get('obiettivo') initial = {} if conversazione_pk: initial['conversazione'] = conversazione_pk if obiettivo_pk: initial['obiettivo'] = obiettivo_pk if request.method == 'POST': form = DocumentoForm(request.POST, request.FILES) if form.is_valid(): doc = form.save(commit=False) doc.caricato_da = request.user doc.save() messages.success(request, 'Documento caricato.') if doc.conversazione: return redirect('conversazione_dettaglio', pk=doc.conversazione.pk) if doc.obiettivo: return redirect('obiettivo_dettaglio', pk=doc.obiettivo.pk) return redirect('documento_dettaglio', pk=doc.pk) else: form = DocumentoForm(initial=initial) return render(request, 'diario/documenti/form.html', {'form': form, 'titolo_pagina': 'Carica documento'}) @login_required def documento_dettaglio(request, pk): doc = get_object_or_404(Documento.objects.select_related('caricato_da', 'conversazione', 'obiettivo'), pk=pk) return render(request, 'diario/documenti/dettaglio.html', { 'doc': doc, 'can_edit': _can_edit(request.user, doc), }) @login_required def documento_elimina(request, pk): doc = get_object_or_404(Documento, pk=pk) if not _can_edit(request.user, doc): return HttpResponseForbidden('Non hai i permessi per eliminare questo documento.') if request.method == 'POST': doc.file.delete(save=False) doc.delete() messages.success(request, 'Documento eliminato.') return redirect('documenti_lista') return render(request, 'diario/conferma_elimina.html', { 'oggetto': doc, 'tipo': 'documento', 'cancel_url': 'documento_dettaglio', }) # ── Ricerca ──────────────────────────────────────────────────────────────────── @login_required def ricerca(request): q = request.GET.get('q', '').strip() risultati = {'conversazioni': [], 'obiettivi': [], 'documenti': [], 'persone': []} total = 0 if len(q) >= 2: risultati['conversazioni'] = Conversazione.objects.filter( Q(titolo__icontains=q) | Q(contenuto__icontains=q) ).select_related('registrato_da')[:20] risultati['obiettivi'] = Obiettivo.objects.filter( Q(titolo__icontains=q) | Q(descrizione__icontains=q) ).select_related('creato_da')[:20] risultati['documenti'] = Documento.objects.filter( Q(titolo__icontains=q) | Q(descrizione__icontains=q) ).select_related('caricato_da')[:20] risultati['persone'] = User.objects.filter( Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(username__icontains=q), is_active=True, )[:20] total = sum(len(v) if hasattr(v, '__len__') else v.count() for v in risultati.values()) ctx = {'q': q, 'risultati': risultati, 'total': total} if getattr(request, 'htmx', False): return render(request, 'diario/ricerca_risultati.html', ctx) return render(request, 'diario/ricerca.html', ctx) # ── Persone ──────────────────────────────────────────────────────────────────── @login_required def persone_lista(request): persone = User.objects.filter(is_active=True).annotate( num_conversazioni=Count('conversazioni_registrate', distinct=True), num_obiettivi=Count('obiettivi_creati', distinct=True), num_documenti=Count('documenti_caricati', distinct=True), ).order_by('first_name', 'username') return render(request, 'diario/persone/lista.html', {'persone': persone}) @login_required def persona_dettaglio(request, pk): persona = get_object_or_404(User, pk=pk, is_active=True) conversazioni = Conversazione.objects.filter( Q(registrato_da=persona) | Q(partecipanti=persona) ).distinct().select_related('registrato_da').order_by('-data')[:20] obiettivi = Obiettivo.objects.filter( Q(creato_da=persona) | Q(assegnato_a=persona) ).distinct().select_related('creato_da').prefetch_related('assegnato_a').order_by('-data_creazione')[:20] documenti = Documento.objects.filter( caricato_da=persona ).select_related('conversazione', 'obiettivo').order_by('-data_caricamento')[:20] return render(request, 'diario/persone/dettaglio.html', { 'persona': persona, 'conversazioni': conversazioni, 'obiettivi': obiettivi, 'documenti': documenti, })