diff --git a/sigi/apps/casas/dashboards.py b/sigi/apps/casas/dashboards.py new file mode 100644 index 0000000..b0027fd --- /dev/null +++ b/sigi/apps/casas/dashboards.py @@ -0,0 +1,73 @@ +import django_filters +from dashboard import Dashcard, getcolor +from django.db.models import Count +from django.utils.translation import gettext as _ +from sigi.apps.casas.models import Orgao +from sigi.apps.servidores.models import Servidor + + +class GerenteFilter(django_filters.FilterSet): + servidor = django_filters.ModelChoiceFilter( + field_name="gerentes_interlegis", + label="Gerente", + queryset=Servidor.objects.exclude(casas_que_gerencia=None), + ) + + class Meta: + model = Orgao + fields = ["servidor"] + + +class CasasGerente(Dashcard): + chart_type = Dashcard.TYPE_DOUGHNUT + title = _("Distribuição de Casas por Gerente") + model = Servidor + label_field = "nome_completo" + datasets = [{"data_field": ("casas_que_gerencia", Count)}] + + def apply_filters(self, request, queryset): + return ( + super() + .apply_filters(request, queryset) + .exclude(casas_que_gerencia=None) + ) + + +class PerformanceCarteira(Dashcard): + chart_type = Dashcard.TYPE_DOUGHNUT + title = _("Performance da gerência de carteiras") + model = Orgao + filterset = GerenteFilter + + LABEL_USAM = _("Utilizam serviços") + LABEL_NAO_USAM = _("Não utilizam servços") + + def apply_filters(self, request, queryset): + filter = self.filterset(request.GET, queryset=queryset) + valid = filter.is_valid() + if filter.form.cleaned_data["servidor"] is None: + if ( + request.user.servidor + and request.user.servidor.casas_que_gerencia.exists() + ): + return request.user.servidor.casas_que_gerencia.all() + return ( + super() + .apply_filters(request, queryset) + .exclude(gerentes_interlegis=None) + ) + + def get_labels(self, request, queryset=None): + return [self.LABEL_USAM, self.LABEL_NAO_USAM] + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + return [ + { + "data": [ + queryset.exclude(servico=None).count(), + queryset.filter(servico=None).count(), + ] + } + ] diff --git a/sigi/apps/convenios/dashboards.py b/sigi/apps/convenios/dashboards.py new file mode 100644 index 0000000..7a9a484 --- /dev/null +++ b/sigi/apps/convenios/dashboards.py @@ -0,0 +1,194 @@ +import numpy as np +import pandas as pd +import django_filters +from dashboard import Dashcard +from django.db.models import Q, Count +from django.utils import timezone +from django.utils.translation import gettext as _ +from sigi.apps.casas.models import TipoOrgao, Orgao +from sigi.apps.convenios.models import Convenio, Projeto + + +def get_tipos(): + tipos = list( + TipoOrgao.objects.filter(sigla__in=["CM", "AL"]).values_list( + "sigla", "nome" + ) + ) + tipos.extend( + [ + ("_legislativo", _("Todo o legislativo")), + ("_outros", _("Demais órgãos")), + ] + ) + return tipos + + +class ResumoConveniosFilter(django_filters.FilterSet): + tipo = django_filters.ChoiceFilter( + field_name="casa_legislativa__tipo__sigla", + label=_("Tipo"), + choices=get_tipos, + method="filter_tipo", + empty_label=None, + initial="CM", + ) + + class Meta: + model = Convenio + fields = ["tipo"] + + def filter_tipo(self, queryset, name, value): + if value == "_legislativo": + tipos = TipoOrgao.objects.filter(legislativo=True).values_list( + "sigla", flat=True + ) + elif value == "_outros": + tipos = TipoOrgao.objects.filter(legislativo=False).values_list( + "sigla", flat=True + ) + else: + tipos = [value] + return queryset.filter(**{f"{name}__in": tipos}) + + +class ResumoConvenios(Dashcard): + chart_type = Dashcard.TYPE_TABLE + title = _("Resumo de informações") + model = Convenio + filterset = ResumoConveniosFilter + template_table = "convenios/dashboard/resumo_convenios.html" + + def apply_filters(self, request, queryset): + if "tipo" not in request.GET: + request.GET = request.GET.copy() + request.GET["tipo"] = "CM" + return super().apply_filters(request, queryset) + + def get_labels(self, request, queryset=None): + return [] + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + filter = self.get_filter(request.GET, queryset) + if filter and filter.is_valid(): + label_tipo = dict(get_tipos())[filter.form.cleaned_data["tipo"]] + else: + label_tipo = dict(get_tipos())["CM"] + convenios = queryset + camaras = filter.filter_tipo( + Orgao.objects.all(), + "tipo__sigla", + filter.form.cleaned_data["tipo"], + ) + convenios_vigentes = convenios.exclude( + data_retorno_assinatura=None + ).filter( + Q(data_termino_vigencia__gte=timezone.localdate()) + | Q(data_termino_vigencia=None) + ) + convenios_andando = convenios.filter(data_retorno_assinatura=None) + convenios_vencidos = convenios.exclude( + Q(data_retorno_assinatura=None) | Q(data_termino_vigencia=None) + ).filter(data_termino_vigencia__lt=timezone.localdate()) + dataset = { + _(f"{label_tipo} com convênios vigentes"): { + k: v + for k, v in convenios_vigentes.values_list( + "projeto__sigla" + ).annotate(Count("casa_legislativa_id", distinct=True)) + }, + _(f"{label_tipo} com convênios em andamento"): { + k: v + for k, v in convenios_andando.values_list( + "projeto__sigla" + ).annotate(Count("casa_legislativa_id", distinct=True)) + }, + _(f"{label_tipo} com convênios vencidos"): { + k: v + for k, v in convenios_vencidos.values_list( + "projeto__sigla" + ).annotate(Count("casa_legislativa_id", distinct=True)) + }, + } + + ds_totais = ( + (_(f"Total de {label_tipo} do país"), camaras.count()), + ( + _(f"Total de {label_tipo} com convênio vigente"), + convenios_vigentes.order_by("casa_legislativa_id") + .distinct("casa_legislativa_id") + .count(), + ), + ( + _(f"Total de {label_tipo} com convênio em andamento"), + convenios_andando.order_by("casa_legislativa_id") + .distinct("casa_legislativa_id") + .count(), + ), + ( + _(f"Total de {label_tipo} com convênio vencido"), + convenios_vencidos.order_by("casa_legislativa_id") + .distinct("casa_legislativa_id") + .count(), + ), + ) + + df = ( + pd.DataFrame.from_dict(dataset, orient="index") + .replace(np.nan, 0) + .convert_dtypes() + ) + + return {"data_frame": df, "totais": ds_totais} + + +class ConvenioServico(Dashcard): + chart_type = Dashcard.TYPE_TABLE + title = _("Convenios e serviços") + model = Orgao + label_name = _("Situação") + + def get_queryset(self, request): + return ( + Orgao.objects.exclude(servico=None) + .filter(servico__data_desativacao=None, convenio=None) + .aggregate( + total=Count("id", distinct=True), + hospedagem=Count( + "id", + filter=Q(servico__tipo_servico__modo="H"), + distinct=True, + ), + ) + ) + + def get_labels(self, request, queryset=None): + return [_("Total")] + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + values = dict(queryset) + datasets = [ + { + "label": _( + "Casas sem convenio que utilizam algum serviço de hospedagem" + ), + "data": [values["hospedagem"]], + }, + { + "label": _( + "Casas sem convenio que utilizam somente serviço de registro" + ), + "data": [values["total"] - values["hospedagem"]], + }, + { + "label": _( + "Casas sem convenio que utilizam algum serviço de registro e/ou hospedagem" + ), + "data": [values["total"]], + }, + ] + return datasets diff --git a/sigi/apps/convenios/templates/convenios/dashboard/resumo_convenios.html b/sigi/apps/convenios/templates/convenios/dashboard/resumo_convenios.html new file mode 100644 index 0000000..27c977e --- /dev/null +++ b/sigi/apps/convenios/templates/convenios/dashboard/resumo_convenios.html @@ -0,0 +1,30 @@ +{% load i18n %} + + + + + {% for item in datasets.data_frame.columns %} + + {% endfor %} + + + + {% for label, values in datasets.data_frame.iterrows %} + + + {% for value in values %} + + {% endfor %} + + {% endfor %} + +
 {{ item }}
{{ label }}{{ value }}
+ + + {% for label, value in datasets.totais %} + + + + + {% endfor %} +
{{ label }}{{ value }}
\ No newline at end of file diff --git a/sigi/apps/eventos/dashboards.py b/sigi/apps/eventos/dashboards.py new file mode 100644 index 0000000..a600108 --- /dev/null +++ b/sigi/apps/eventos/dashboards.py @@ -0,0 +1,153 @@ +import calendar +import datetime +import django_filters +from dashboard import Dashcard, getcolor +from django.db.models import F, Count, Q +from django.utils import timezone +from django.utils.translation import gettext as _ +from sigi.apps.eventos.models import Evento, TipoEvento + + +def get_anos(): + return [ + (str(a), str(a)) + for a in Evento.objects.filter(status=Evento.STATUS_REALIZADO) + .order_by("data_inicio__year") + .values_list("data_inicio__year", flat=True) + .distinct("data_inicio__year") + ] + + +class AnoFilterset(django_filters.FilterSet): + ano = django_filters.ChoiceFilter( + field_name="data_inicio", + lookup_expr="year", + label=_("Ano"), + distinct=True, + choices=get_anos, + ) + + class Meta: + model = Evento + fields = ["ano"] + + +class EventosStatus(Dashcard): + chart_type = Dashcard.TYPE_DOUGHNUT + title = _("Eventos por status") + model = Evento + label_field = ("status", F, lambda s: dict(Evento.STATUS_CHOICES)[s]) + datasets = [{"data_field": ("id", Count)}] + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) + + +class EventosAno(Dashcard): + chart_type = Dashcard.TYPE_LINE + title = _("Eventos nos últimos 12 meses") + model = TipoEvento + + def get_dataset_color(self, dataset_label): + print(f"EventosAno: {dataset_label}, {getcolor(dataset_label)}") + return getcolor(dataset_label) + + def get_meses(self, request=None): + if request is None: + mes = timezone.localdate().month + ano = timezone.localdate().year + else: + mes = int(request.GET.get("mes", timezone.localdate().month)) + ano = int(request.GET.get("ano", timezone.localdate().year)) + + mes_ano = datetime.date( + year=ano, month=mes, day=1 + ) + datetime.timedelta(days=calendar.monthrange(ano, mes)[1]) + + meses = [] + + for i in range(13): + meses.append(mes_ano) + mes_ano = (mes_ano - datetime.timedelta(days=1)).replace(day=1) + meses.reverse() + + return meses + + def get_labels(self, request, queryset=None): + return [f"{m:%m/%Y}" for m in self.get_meses(request)[:-1]] + + def get_counters(self, request): + counts = {} + for mes in self.get_meses(request)[:-1]: + counts[f"count_{mes:%Y_%m}"] = Count( + "evento", + Q( + evento__data_inicio__year=mes.year, + evento__data_inicio__month=mes.month, + ), + ) + return counts + + def apply_filters(self, request, queryset): + return ( + super() + .apply_filters(request, queryset) + .filter(evento__status=Evento.STATUS_REALIZADO) + ) + + def get_queryset(self, request): + counts = self.get_counters(request) + return ( + super() + .get_queryset(request) + .values("categoria") + .annotate(**counts) + ) + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + categorias = dict(TipoEvento.CATEGORIA_CHOICES) + counters = self.get_counters(request).keys() + return [ + { + "label": categorias[r["categoria"]], + "data": [r[c] for c in counters], + "backgroundColor": self.get_dataset_color(r["categoria"]), + } + for r in queryset + ] + + def get_next_page(self, request=None, queryset=None): + mes = self.get_meses(request)[-1] + return f"ano={mes.year}&mes={mes.month}" + + def get_prev_page(self, request=None, queryset=None): + mes = self.get_meses(request)[-3] + return f"ano={mes.year}&mes={mes.month}" + + +class EventosCategoria(Dashcard): + chart_type = Dashcard.TYPE_DOUGHNUT + title = _("Eventos por categoria") + filterset = AnoFilterset + model = Evento + label_field = ( + "tipo_evento__categoria", + F, + lambda x: dict(TipoEvento.CATEGORIA_CHOICES)[x], + ) + datasets = [{"data_field": ("id", Count)}] + + def apply_filters(self, request, queryset): + return ( + super() + .apply_filters(request, queryset) + .filter(status=Evento.STATUS_REALIZADO) + ) + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) diff --git a/sigi/apps/servicos/dashboards.py b/sigi/apps/servicos/dashboards.py new file mode 100644 index 0000000..d4a2403 --- /dev/null +++ b/sigi/apps/servicos/dashboards.py @@ -0,0 +1,393 @@ +import calendar +import datetime +import locale +from dashboard import Dashcard, getcolor +from random import randint, seed +from django.db.models import Count, F, Q +from django.db.models.functions import TruncMonth +from django.http import QueryDict +from django.utils import timezone +from django.utils.text import slugify +from django.utils.translation import gettext as _, to_locale, get_language +import django_filters +from sigi.apps.servicos.models import Servico, TipoServico +from sigi.apps.contatos.models import UnidadeFederativa + + +class UsoServicosFilter(django_filters.FilterSet): + uf = django_filters.ModelChoiceFilter( + field_name="servico__casa_legislativa__municipio__uf", + label=_("UF"), + queryset=UnidadeFederativa.objects.all(), + ) + + class Meta: + model = TipoServico + fields = ["uf"] + + +class AnoServicoFilter(django_filters.FilterSet): + ano = django_filters.ModelChoiceFilter( + field_name="data_ativacao__year", + label=_("Ano"), + queryset=( + Servico.objects.filter(hospedagem_interlegis=True) + .order_by("data_ativacao__year") + .values_list("data_ativacao__year", flat=True) + .distinct("data_ativacao__year") + ), + ) + + class Meta: + model = Servico + fields = ["ano"] + + +class Sazonalidade(Dashcard): + title = _("Sazonalidade da hospedagem de serviços") + chart_type = Dashcard.TYPE_LINE + model = Servico + label_field = ("data_ativacao", TruncMonth, lambda d: d.strftime("%m/%Y")) + datasets = [ + { + "label_field": "tipo_servico__sigla", + "data_field": ("*", Count), + } + ] + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) + + def get_queryset(self, request): + qs = ( + super() + .get_queryset(request) + .filter(data_desativacao=None) + .order_by("tipo_servico__sigla", "data_ativacao") + ) + ano = request.GET.get("ano", None) + if ano is None: + ano = ( + qs.dates("data_ativacao", "year") + .values_list("data_ativacao__year", flat=True) + .last() + ) + return qs.filter(data_ativacao__year=ano) + + def get_prev_page(self, request=None, queryset=None): + anos = Servico.objects.dates("data_ativacao", "year").values_list( + "data_ativacao__year", flat=True + ) + if request is None: + params = QueryDict().copy() + params["ano"] = anos.last() - 1 + else: + params = request.GET.copy() + params["ano"] = int(request.GET.get("ano", anos.last())) - 1 + if params["ano"] not in anos: + return None + return params.urlencode() + + def get_next_page(self, request=None, queryset=None): + if request is None: + return None + anos = Servico.objects.dates("data_ativacao", "year").values_list( + "data_ativacao__year", flat=True + ) + params = request.GET.copy() + params["ano"] = int(request.GET.get("ano", anos.last())) + 1 + if params["ano"] not in anos: + return None + return params.urlencode() + + +class ResumoSeit(Dashcard): + title = _("Serviços hospedados no Interlegis") + chart_type = Dashcard.TYPE_TABLE + label_name = _("Serviço") + model = Servico + + def get_meses(self, request=None): + if request is None: + mes = datetime.date.today().month + ano = datetime.date.today().year + else: + mes = int(request.GET.get("mes", datetime.date.today().month)) + ano = int(request.GET.get("ano", datetime.date.today().year)) + mes_atual = datetime.date(year=ano, month=mes, day=1) + mes_anterior = (mes_atual - datetime.timedelta(days=1)).replace(day=1) + mes_proximo = mes_atual + datetime.timedelta( + days=calendar.monthrange(mes_atual.year, mes_atual.month)[1] + ) + + return mes_atual, mes_anterior, mes_proximo + + def get_queryset(self, request): + mes_atual, mes_anterior, mes_proximo = self.get_meses(request) + return ( + super() + .get_queryset(request) + .filter( + ( + Q(data_ativacao__year=mes_atual.year) + & Q(Q(data_ativacao__month=mes_atual.month)) + ) + | ( + Q(data_ativacao__year=mes_anterior.year) + & Q(Q(data_ativacao__month=mes_anterior.month)) + ) + ) + .values( + servico=F("tipo_servico__nome"), + mes=TruncMonth("data_ativacao"), + ) + .annotate(ativados=Count("casa_legislativa__id", distinct=True)) + ) + + def get_labels(self, request, queryset=None): + mes_atual, mes_anterior, mes_proximo = self.get_meses(request) + return [ + _("Total de casas atendidas"), + _(f"Novas casas em {mes_anterior:%m/%Y}"), + _(f"Novas casas em {mes_atual:%m/%Y}"), + ] + + def get_datasets(self, request, queryset=None): + mes_atual, mes_anterior, mes_proximo = self.get_meses(request) + if queryset is None: + queryset = self.get_queryset(request) + labels = self.get_labels(request, queryset) + datasets = { + s["servico"]: { + "total": s["total"], + mes_anterior: 0, + mes_atual: 0, + } + for s in Servico.objects.filter(data_desativacao=None) + .values(servico=F("tipo_servico__nome")) + .annotate(total=Count("casa_legislativa", distinct=True)) + } + for data in queryset: + datasets[data["servico"]][data["mes"]] = data["ativados"] + + return [ + { + "label": label, + "data": { + labels[0]: data["total"], + labels[1]: data[mes_atual], + labels[2]: data[mes_anterior], + }, + } + for label, data in datasets.items() + ] + + def get_prev_page(self, request=None, queryset=None): + mes_atual, mes_anterior, mes_proximo = self.get_meses(request) + params = QueryDict().copy() + params["ano"] = mes_anterior.year + params["mes"] = mes_anterior.month + return params.urlencode() + + def get_next_page(self, request=None, queryset=None): + mes_atual, mes_anterior, mes_proximo = self.get_meses(request) + params = QueryDict().copy() + params["ano"] = mes_proximo.year + params["mes"] = mes_proximo.month + return params.urlencode() + + +class AtualizacaoServicos(Dashcard): + title = _("Frequência de atualização") + chart_type = Dashcard.TYPE_BAR + model = TipoServico + + intervalos = [ + ("Na semana", 7), + ("No mês", 30), + ("No trimestre", 3 * 30), + ("No semestre", 6 * 30), + ("No ano", 365), + ("Mais de ano", None), + ] + + def get_queryset(self, request): + counts = {} + hoje = timezone.localdate() + ate = hoje + + for label, dias in self.intervalos: + if dias is not None: + de = hoje - datetime.timedelta(days=dias) + counts[slugify(label)] = Count( + "servico", Q(servico__data_ultimo_uso__range=(de, ate)) + ) + ate = de - datetime.timedelta(days=1) + else: + counts[slugify(label)] = Count( + "servico", Q(servico__data_ultimo_uso__lte=ate) + ) + + return ( + super() + .get_queryset(request) + .exclude(string_pesquisa="") + .filter(servico__data_desativacao=None) + .annotate(**counts) + ) + + def get_labels(self, request, queryset=None): + return [label for label, *__ in self.intervalos] + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + return [ + { + "type": "bar", + "label": ts.sigla, + "data": { + label: getattr(ts, slugify(label)) + for label, *__ in self.intervalos + }, + "backgroundColor": getcolor(ts.sigla), + } + for ts in queryset + ] + + +class UsoServicos(Dashcard): + title = _("Uso dos serviços") + chart_type = Dashcard.TYPE_BAR + model = TipoServico + filterset = UsoServicosFilter + label_field = "sigla" + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) + + def apply_filters(self, request, queryset): + queryset = queryset.exclude(string_pesquisa="").filter( + servico__data_desativacao=None + ) + return super().apply_filters(request, queryset) + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + + counts = { + f"{key}_count": Count( + "servico", + distinct=True, + filter=Q(servico__resultado_verificacao=key), + ) + for key, *__ in Servico.RESULTADO_CHOICES + } + + queryset = queryset.annotate(**counts) + + return [ + { + "label": label, + "data": { + r.sigla: getattr(r, f"{key}_count") for r in queryset + }, + } + for key, label in Servico.RESULTADO_CHOICES + ] + + +class ServicosAno(Dashcard): + title = _("Serviços hospedados por ano") + chart_type = Dashcard.TYPE_BAR + model = Servico + filterset = AnoServicoFilter + chart_options = { + "scales": {"x": {"stacked": True}, "y": {"stacked": True}}, + "plugins": {"tooltip": {"mode": "index"}}, + } + + def get_dataset_color(self, dataset_label): + return getcolor(dataset_label) + + def apply_filters(self, request, queryset): + return ( + super() + .apply_filters(request, queryset) + .filter(hospedagem_interlegis=True) + ) + + def get_queryset(self, request): + qs = super().get_queryset(request) + if request.GET.get("ano", None): + # Usuário informou um ano, então vamos mostrar os meses daquele ano + qs = ( + qs.order_by("data_ativacao__month", "tipo_servico__sigla") + .values( + label=F("data_ativacao__month"), + sigla=F("tipo_servico__sigla"), + ) + .annotate(total=Count("id")) + ) + else: + qs = ( + qs.order_by("data_ativacao__year", "tipo_servico__sigla") + .values( + label=F("data_ativacao__year"), + sigla=F("tipo_servico__sigla"), + ) + .annotate(total=Count("id")) + ) + return qs + + def get_labels(self, request, queryset=None): + if queryset is None: + return list( + Servico.objects.filter(hospedagem_interlegis=True) + .order_by("data_ativacao__year") + .values_list("data_ativacao__year") + .distinct("data_ativacao__year") + ) + + if request.GET.get("ano", None): + lang = to_locale(get_language()) + ".UTF-8" + locale.setlocale(locale.LC_ALL, lang) + map_function = lambda x: _(calendar.month_abbr[x]) + else: + map_function = str + + labels = list({r["label"] for r in queryset}) + labels.sort() + labels = list(map(map_function, labels)) + + return labels + + def get_datasets(self, request, queryset=None): + if queryset is None: + queryset = self.get_queryset(request) + if request.GET.get("ano", None): + lang = to_locale(get_language()) + ".UTF-8" + locale.setlocale(locale.LC_ALL, lang) + map_function = lambda x: _(calendar.month_abbr[x]) + else: + map_function = str + + labels = self.get_labels(request, queryset) + + series = {} + for d in queryset: + sigla = d["sigla"] + label = map_function(d["label"]) + if sigla not in series: + series[sigla] = dict(zip(labels, [0] * len(labels))) + series[sigla][label] = d["total"] + + return [ + { + "label": s, + "data": series[s], + "backgroundColor": getcolor(s), + } + for s in series + ]