BatchType | Autorização de usuários | Fatores de Consumo
Com a finalização da estrutura da tela de configuração da fazenda, três recursos foram implementados: 1 - Fatores de consumo: Agora é possível definir três fatores diferentes, o consumo padrão (ainda passará por ajustes) que representará o consumo do animal com base em seu peso, o consumo no verão, valor utilizado para correção de consumo nos meses de verão e consumo de inverno segundo a mesma lógica da informação anterior; 2 - BatchTypes: Tipos de lote, permite ao usuário, na tela de configuração da sua fazenda, criar tipos para os lotes, os quais carregam um multiplicador de consumo, utilizado futuramente na geração de relatório como uma camada de correção a mais nos valores; 3 - Permissão de acesso para outros usuários: A tela de configuração da fazenda agora permite que sejam inclusos outros usuários na fazenda selecionada, carregando as mesmas permissões e capacidades do dono.
This commit is contained in:
1 parent
8d61dc60a8
commit
740547a8ed
22 files changed
+661
-60
No files matched your search
@@ -42,3 +42,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Blocks scroll form changing the value on inputs type number
|
||||
document.querySelectorAll('input[type="number"]').forEach(input => {
|
||||
input.addEventListener('wheel', function(event) {
|
||||
event.target.blur();
|
||||
});
|
||||
});
|
||||
@@ -11,10 +11,12 @@
|
||||
|
||||
<link rel="stylesheet" href="{% static 'core/css/reset.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'core/css/variables.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'core/css/components/buttons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
|
||||
|
||||
<!-- Templates based on base_model can't pass their stylesheet on block extra_css -->
|
||||
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.contrib import admin
|
||||
|
||||
from .models.user import User
|
||||
from .models.farm import Farm, PaddockGroup, Paddock
|
||||
from .models.animal import Animal, Batch, BatchAnimal, FeedingRecord
|
||||
from .models.animal import Animal, BatchType, Batch, BatchAnimal, FeedingRecord
|
||||
from .models.rotation import Rotation
|
||||
|
||||
admin.site.register([
|
||||
@@ -15,4 +15,5 @@ admin.site.register([
|
||||
BatchAnimal,
|
||||
FeedingRecord,
|
||||
Rotation,
|
||||
BatchType
|
||||
])
|
||||
@@ -1,5 +1,5 @@
|
||||
from django import forms
|
||||
from ..models.animal import Batch, Animal
|
||||
from ..models.animal import Batch, Animal, BatchType
|
||||
|
||||
class AnimalCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -43,3 +43,14 @@ class BatchForm(forms.ModelForm):
|
||||
'animals': 'Animais',
|
||||
}
|
||||
BatchCreateForm = BatchEditForm = BatchForm # Alias to BatchForm since its implementations are the same
|
||||
|
||||
class BatchTypeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BatchType
|
||||
fields = ['name', 'consumption_factor']
|
||||
|
||||
labels = {
|
||||
'name': 'Nome',
|
||||
'consumption_factor': 'Fator de consumo',
|
||||
}
|
||||
BatchTypeCreateForm = BatchTypeEditForm = BatchTypeForm
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.0.2 on 2026-05-30 03:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('entities', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BatchType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('name', models.CharField(max_length=45)),
|
||||
('consumption_factor', models.DecimalField(decimal_places=2, default=1.0, max_digits=4)),
|
||||
('farm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batch_types', to='entities.farm')),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='batch_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='batches', to='entities.batchtype'),
|
||||
),
|
||||
]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.2 on 2026-05-30 19:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('entities', '0002_batchtype_alter_batch_batch_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='farm',
|
||||
name='default_consumption',
|
||||
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='farm',
|
||||
name='summer_consumption',
|
||||
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='farm',
|
||||
name='winter_consumption',
|
||||
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||
),
|
||||
]
|
||||
@@ -36,6 +36,10 @@ class Animal(models.Model):
|
||||
else:
|
||||
return "Menos de um mês" # Less than a month
|
||||
|
||||
@property
|
||||
def is_in_batch(self):
|
||||
return self.batchanimal_set.filter(is_active=True).exists()
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
@@ -45,11 +49,25 @@ class Animal(models.Model):
|
||||
)
|
||||
]
|
||||
|
||||
class BatchType(models.Model):
|
||||
is_active = models.BooleanField(default=True)
|
||||
farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='batch_types')
|
||||
name = models.CharField(max_length=45)
|
||||
consumption_factor = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
||||
|
||||
def __str__(self):
|
||||
state = 'Ativo' if self.is_active else 'Inativo'
|
||||
return f"{state} - {self.name}, {self.consumption_factor}"
|
||||
|
||||
@property
|
||||
def consumption_percentage(self):
|
||||
return int((self.consumption_factor - 1) * 100)
|
||||
|
||||
class Batch(models.Model):
|
||||
is_active = models.BooleanField(default=True)
|
||||
identifier = models.CharField(max_length=45)
|
||||
animal_category = models.CharField(max_length=45, null=True, blank=True)
|
||||
batch_type = models.CharField(max_length=45, null=True, blank=True)
|
||||
batch_type = models.ForeignKey(BatchType, on_delete=models.SET_NULL, related_name='batches', null=True, blank=True)
|
||||
animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -94,6 +112,16 @@ class BatchAnimal(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.animal.identifier} in {self.batch.identifier}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_active_changed = self.__original_is_active != self.is_active
|
||||
if is_active_changed and self.is_active:
|
||||
raise ValueError(
|
||||
"Reactivating a BatchAnimal is not allowed automatically because it may violate "
|
||||
"the unique active batch constraint for an animal. Review the logic in "
|
||||
"models/animal.py (BatchAnimal.save) before proceeding."
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class FeedingRecord(models.Model):
|
||||
animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='feeding_record')
|
||||
rotation = models.ForeignKey('entities.Rotation', on_delete=models.CASCADE, related_name='feeding_record')
|
||||
|
||||
@@ -4,10 +4,27 @@ class Farm(models.Model):
|
||||
is_active = models.BooleanField(default=True)
|
||||
name = models.CharField(max_length=45)
|
||||
total_area = models.IntegerField(null=True, blank=True)
|
||||
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
||||
summer_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
||||
winter_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def animal_count(self):
|
||||
return self.animals.filter(
|
||||
is_active=True
|
||||
).count()
|
||||
|
||||
@property
|
||||
def paddock_count(self):
|
||||
return self.paddocks.filter(
|
||||
is_active=True
|
||||
).count()
|
||||
|
||||
|
||||
class PaddockGroup(models.Model):
|
||||
is_active = models.BooleanField(default=True)
|
||||
identifier = models.CharField(max_length=45)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
|
||||
from ..models import Animal, Batch
|
||||
from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm
|
||||
from ..models import Animal, Batch, BatchType
|
||||
from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm, BatchTypeCreateForm, BatchTypeEditForm
|
||||
from entities.models import Farm
|
||||
|
||||
# Local utility functions
|
||||
@@ -15,15 +15,36 @@ def _get_selected_farm(request):
|
||||
def _get_animal_by_id(request, animal_id):
|
||||
return get_object_or_404(Animal, pk=animal_id, farm_id=_get_selected_farm_id(request))
|
||||
|
||||
def _get_farms_active_animals(request):
|
||||
return Animal.objects.filter(
|
||||
farm=_get_selected_farm_id(request),
|
||||
is_active=True
|
||||
)
|
||||
# def _get_farms_active_animals(request):
|
||||
# return Animal.objects.filter(
|
||||
# farm=_get_selected_farm_id(request),
|
||||
# is_active=True
|
||||
# )
|
||||
|
||||
def _get_batch_by_id(request, batch_id):
|
||||
return get_object_or_404(Batch, pk=batch_id, is_active=True)
|
||||
|
||||
def _get_farms_batch_types(request):
|
||||
return BatchType.objects.filter(is_active=True, farm_id=_get_selected_farm_id(request))
|
||||
|
||||
def _get_no_batch_farms_animals(request):
|
||||
return Animal.objects.filter(
|
||||
farm_id=_get_selected_farm_id(request),
|
||||
is_active=True
|
||||
).exclude(
|
||||
batchanimal__is_active=True
|
||||
)
|
||||
|
||||
def _get_animals_from_batch(batch_id):
|
||||
return Batch.objects.get(
|
||||
id=batch_id
|
||||
).animals.filter(
|
||||
batchanimal__is_active=True
|
||||
)
|
||||
|
||||
def _get_batchtype_by_id(request, batchtype_id):
|
||||
return get_object_or_404(BatchType, pk=batchtype_id, is_active=True, farm=_get_selected_farm(request))
|
||||
|
||||
# Animal Services
|
||||
def get_animal_create_context():
|
||||
form = AnimalCreateForm()
|
||||
@@ -92,7 +113,8 @@ def delete_animal(request, animal_id):
|
||||
# Batch Services
|
||||
def get_batch_create_context(request):
|
||||
form = BatchForm()
|
||||
form.fields['animals'].queryset = _get_farms_active_animals(request)
|
||||
form.fields['animals'].queryset = _get_no_batch_farms_animals(request)
|
||||
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
|
||||
|
||||
context = {
|
||||
'title': 'Novo Lote',
|
||||
@@ -117,8 +139,12 @@ def create_batch(request):
|
||||
|
||||
def get_batch_update_context(request, batch_id):
|
||||
batch_old = _get_batch_by_id(request, batch_id)
|
||||
animals_querset = _get_no_batch_farms_animals(request) | _get_animals_from_batch(batch_id)
|
||||
animals_querset = animals_querset.distinct()
|
||||
|
||||
form = BatchForm(instance=batch_old)
|
||||
form.fields['animals'].queryset = _get_farms_active_animals(request)
|
||||
form.fields['animals'].queryset = animals_querset
|
||||
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
|
||||
|
||||
context = {
|
||||
'title': f'Editar Lote {batch_old.identifier}',
|
||||
@@ -164,3 +190,75 @@ def delete_batch(request, batch_id):
|
||||
batch = _get_batch_by_id(request, batch_id)
|
||||
batch.is_active = False
|
||||
batch.save()
|
||||
|
||||
# Batch Type Services
|
||||
def get_batchtype_create_context(request):
|
||||
form = BatchTypeCreateForm()
|
||||
|
||||
context = {
|
||||
'title': 'Novo Tipo de Lote',
|
||||
'form': form,
|
||||
'action_url': reverse('entities:animal:batchtype_create'),
|
||||
'return_url': reverse('entities:farm:farm_home'),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def create_batchtype(request):
|
||||
form = BatchTypeCreateForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
batchtype = form.save(commit=False)
|
||||
batchtype.farm = _get_selected_farm(request)
|
||||
batchtype.save()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_batchtype_update_context(request, batchtype_id):
|
||||
batchtype = _get_batchtype_by_id(request, batchtype_id)
|
||||
form = BatchTypeEditForm(instance=batchtype)
|
||||
|
||||
context = {}
|
||||
context['title'] = 'Editar tipo de lote'
|
||||
context['action_url'] = reverse(
|
||||
'entities:animal:batchtype_update',
|
||||
kwargs={'batchtype_id': batchtype.pk}
|
||||
)
|
||||
context['return_url'] = reverse('entities:farm:farm_home')
|
||||
context['form'] = form
|
||||
|
||||
return context
|
||||
|
||||
def update_batchtype(request, batchtype_id):
|
||||
batchtype = _get_batchtype_by_id(request, batchtype_id)
|
||||
form = BatchTypeEditForm(request.POST, instance=batchtype)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_batchtype_delete_context(request, batchtype_id):
|
||||
batchtype = _get_batchtype_by_id(request, batchtype_id)
|
||||
context = {}
|
||||
context['description'] = f"Excluir tipo de lote {batchtype.name}?"
|
||||
context['positive_url'] = reverse('entities:animal:batchtype_delete', kwargs={'batchtype_id': batchtype_id})
|
||||
context['negative_url'] = reverse('entities:farm:farm_home')
|
||||
|
||||
return context
|
||||
|
||||
def delete_batchtype(request, batchtype_id):
|
||||
batchtype = _get_batchtype_by_id(request, batchtype_id)
|
||||
|
||||
Batch.objects.filter(
|
||||
is_active=True,
|
||||
batch_type=batchtype
|
||||
).update(
|
||||
batch_type=None
|
||||
)
|
||||
|
||||
batchtype.is_active = False
|
||||
batchtype.save()
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.db.models import Count, Q
|
||||
from decimal import Decimal
|
||||
|
||||
from ..models import Farm, Paddock, PaddockGroup
|
||||
from ..models import Farm, Paddock, PaddockGroup, BatchType, User
|
||||
from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm
|
||||
|
||||
|
||||
@@ -43,20 +45,36 @@ def _get_paddock_group_by_id(request, paddock_group_id):
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def _get_farms_batch_types(request):
|
||||
return BatchType.objects.filter(is_active=True, farm=_get_selected_farm(request))
|
||||
|
||||
# Farm Services
|
||||
def get_farm_home_context(request):
|
||||
authenticated_user = request.user
|
||||
selected_farm = _get_selected_farm(request)
|
||||
|
||||
farms_query = authenticated_user.farms.filter(is_active=True)
|
||||
farms_with_counts = farms_query.annotate(
|
||||
total_animals=Count(
|
||||
'animals',
|
||||
filter=Q(animals__is_active=True),
|
||||
distinct=True
|
||||
),
|
||||
total_paddocks=Count(
|
||||
'paddocks',
|
||||
filter=Q(paddocks__is_active=True),
|
||||
distinct=True
|
||||
)
|
||||
)
|
||||
allowed_users = selected_farm.users.exclude(pk=authenticated_user.pk)
|
||||
batch_types = _get_farms_batch_types(request)
|
||||
|
||||
context = {}
|
||||
context['title'] = 'Fazenda'
|
||||
|
||||
farms = Farm.objects.filter(
|
||||
users=authenticated_user,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
context['farms'] = farms
|
||||
context['title'] = selected_farm.name
|
||||
context['selected_farm'] = selected_farm
|
||||
context['farms'] = farms_with_counts
|
||||
context['allowed_users'] = allowed_users
|
||||
context['batch_types'] = batch_types
|
||||
|
||||
return context
|
||||
|
||||
@@ -67,7 +85,7 @@ def get_farm_create_context():
|
||||
context['title'] = 'Nova fazenda'
|
||||
context['form'] = form
|
||||
context['action_url'] = reverse('entities:farm:farm_create')
|
||||
context['return_url'] = reverse('entities:farm:farm_list')
|
||||
context['return_url'] = reverse('entities:farm:farm_home')
|
||||
|
||||
return context
|
||||
|
||||
@@ -92,7 +110,7 @@ def get_update_farm_context(request, farm_id):
|
||||
'entities:farm:farm_update',
|
||||
kwargs={'farm_id': farm.pk}
|
||||
)
|
||||
context['return_url'] = reverse('entities:farm:farm_list')
|
||||
context['return_url'] = reverse('entities:farm:farm_home')
|
||||
context['form'] = form
|
||||
|
||||
return context
|
||||
@@ -116,7 +134,7 @@ def get_farm_delete_context(request, farm_id):
|
||||
'entities:farm:farm_delete',
|
||||
kwargs={'farm_id': farm_id}
|
||||
)
|
||||
context['negative_url'] = reverse('entities:farm:farm_list')
|
||||
context['negative_url'] = reverse('entities:farm:farm_home')
|
||||
|
||||
return context
|
||||
|
||||
@@ -125,6 +143,17 @@ def delete_farm(request, farm_id):
|
||||
farm.is_active = False
|
||||
farm.save()
|
||||
|
||||
def update_consumption(request):
|
||||
default_consumption = Decimal(request.POST.get('default-consume'))
|
||||
summer_consumption = Decimal(request.POST.get('summer-consume'))
|
||||
winter_consumption = Decimal(request.POST.get('winter-consume'))
|
||||
|
||||
selected_farm = _get_selected_farm(request)
|
||||
selected_farm.default_consumption = default_consumption
|
||||
selected_farm.summer_consumption = summer_consumption
|
||||
selected_farm.winter_consumption = winter_consumption
|
||||
|
||||
selected_farm.save()
|
||||
|
||||
# Paddock Services
|
||||
def get_paddock_create_context():
|
||||
@@ -271,3 +300,40 @@ def delete_paddock_group(request, paddock_group_id):
|
||||
paddock_group = _get_paddock_group_by_id(request, paddock_group_id)
|
||||
paddock_group.is_active = False
|
||||
paddock_group.save()
|
||||
|
||||
# User Authorize Services
|
||||
def authorize_user(request, user_id):
|
||||
farm = _get_selected_farm(request)
|
||||
user = get_object_or_404(User, id=user_id, is_active=True)
|
||||
user.farms.add(farm)
|
||||
|
||||
def get_deauthorize_user_context(request, user_id):
|
||||
user = get_object_or_404(User, id=user_id, is_active=True)
|
||||
|
||||
context = {}
|
||||
context['description'] = f"Remover acesso de {user.short_name}?"
|
||||
context['positive_url'] = reverse(
|
||||
'entities:farm:farm_user_deauthorize',
|
||||
kwargs={'user_id': user_id}
|
||||
)
|
||||
context['negative_url'] = reverse('entities:farm:farm_home')
|
||||
|
||||
return context
|
||||
|
||||
def deauthorize_user(request, user_id):
|
||||
farm = _get_selected_farm(request)
|
||||
user = get_object_or_404(User, id=user_id, is_active=True)
|
||||
user.farms.remove(farm)
|
||||
|
||||
def get_user_card_context(request):
|
||||
context = {}
|
||||
informed_username = request.GET.get('username')
|
||||
|
||||
found_user = User.objects.filter(login=informed_username).first()
|
||||
|
||||
if found_user.farms.contains(_get_selected_farm(request)):
|
||||
context['already_allowed'] = 'True'
|
||||
else:
|
||||
context['found_user'] = found_user
|
||||
|
||||
return context
|
||||
@@ -0,0 +1,120 @@
|
||||
.user-authorize-title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-search {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.user-search label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-search input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.user-search input:focus {
|
||||
outline: none;
|
||||
border-color: #198754;
|
||||
}
|
||||
|
||||
.user-search button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
background: #198754;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.user-search button:hover {
|
||||
background: #157347;
|
||||
}
|
||||
|
||||
.user-card-container {
|
||||
margin-top: 20px;
|
||||
min-height: 150px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-card .name {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-card .name i {
|
||||
margin-right: 6px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.user-card .alert {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.user-card button {
|
||||
align-self: flex-end;
|
||||
text-decoration: none;
|
||||
background: #198754;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.user-card button:hover {
|
||||
background: #157347;
|
||||
}
|
||||
|
||||
.failed-user-find {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
color: #dc3545;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.failed-user-find p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.already-allowed-user {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
color: #198754;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.already-allowed-user p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
let isAnimating = false;
|
||||
|
||||
container.addEventListener("wheel", (event) => {
|
||||
let isMobile = window.innerWidth <= 768;
|
||||
if (isMobile) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const maxScroll =
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'layouts/base_modal.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'animal/css/batch.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
NOVO TIPO DE LOTE <br>
|
||||
{% include 'components/form.html' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'layouts/base_modal.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'animal/css/animal.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
EDITAR TIPO DE LOTE <br>
|
||||
{% include 'components/form.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,46 +1,105 @@
|
||||
{% extends 'layouts/base.html' %}
|
||||
{% load static %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'farm/css/farm_home.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="farm_title">
|
||||
<h1>Fazendas</h1>
|
||||
<form method="GET" action="{% url 'entities:farm:farm_home' %}" class="form_farm_search">
|
||||
{% if filter_query %}
|
||||
<a href="{% url 'entities:farm:farm_home' %}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a>
|
||||
{% endif %}
|
||||
<label for="farm_search">Buscar</label>
|
||||
<input type="text" name="farm_search" required>
|
||||
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
|
||||
</form>
|
||||
<a href="{% url 'entities:farm:farm_create' %}">Novo <i class="fa-solid fa-plus"></i></a>
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
<p>Configurações da fazenda</p>
|
||||
</div>
|
||||
{% if not farms %}
|
||||
<p>Nenhuma fazenda encontrado.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Área total</th>
|
||||
<th>Editar</th>
|
||||
<th>Excluir</th>
|
||||
</tr>
|
||||
{% for farm in farms %}
|
||||
<tr>
|
||||
<td>{{ farm.name }}</td>
|
||||
<td>{{ farm.total_area }} m2</td>
|
||||
<td><a href="{% url 'entities:farm:farm_update' farm.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
|
||||
<td>
|
||||
<form method="POST" action="{% url 'entities:farm:farm_delete' farm.id %}">
|
||||
<div class="header-right">
|
||||
<div class="farm-info">
|
||||
<p>Animais</p>
|
||||
<p>{{ selected_farm.animal_count }}</p>
|
||||
</div>
|
||||
<div class="farm-info">
|
||||
<p>Piquetes</p>
|
||||
<p>{{ selected_farm.paddock_count }}</p>
|
||||
</div>
|
||||
<div class="farm-info">
|
||||
<p>Área</p>
|
||||
<p>{{ selected_farm.total_area }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="farm-config-container">
|
||||
<h2 class="farm-config-title">Fatores de consumo</h2>
|
||||
<form action="{% url "entities:farm:consumption_update" %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
|
||||
<div class="config-card">
|
||||
<p class="config-card-title">Consumo padrão</p>
|
||||
<input type="number" name="default-consume" min="0.01" step="0.01" value="{{ selected_farm.default_consumption|unlocalize }}">
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<p class="config-card-title">Consumo no verão</p>
|
||||
<input type="number" name="summer-consume" min="0.01" step="0.01" value="{{ selected_farm.summer_consumption|unlocalize }}">
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<p class="config-card-title">Consumo no inverno</p>
|
||||
<input type="number" name="winter-consume" min="0.01" step="0.01" value="{{ selected_farm.winter_consumption|unlocalize }}">
|
||||
</div>
|
||||
<div class="config-submit">
|
||||
<button type="submit">Salvar</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
<div class="batch-type-config-container">
|
||||
<div class="batch-type-config-header">
|
||||
<h2 class="batch-type-title">Tipos de lote</h2>
|
||||
<a hx-get="{% url "entities:animal:batchtype_create" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Tipo</a>
|
||||
</div>
|
||||
{% for type in batch_types %}
|
||||
<div class="batch-type-card">
|
||||
<div class="batch-type-left">
|
||||
<p class="type-title">{{ type.name }}</p>
|
||||
<p class="type-consumption-factor">Multiplicativo de consumo: {{ type.consumption_factor }} ({{ type.consumption_percentage }}%)</p>
|
||||
</div>
|
||||
<div class="batch-type-right">
|
||||
<a class="edit-type" hx-get="{% url "entities:animal:batchtype_update" type.id %}" hx-swap="beforeend" hx-target="body">Editar</a>
|
||||
<a class="delete-type" hx-get="{% url "entities:animal:batchtype_delete" type.id %}" hx-swap="beforeend" hx-target="body">Excluir</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="allowed-users-container">
|
||||
<div class="allowed-users-header">
|
||||
<h2 class="allowed-users-title">Usuários com acesso</h2>
|
||||
<a hx-get="{% url "entities:farm:new_authorization" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Permissão</a>
|
||||
</div>
|
||||
{% if allowed_users|length < 1 %}
|
||||
<p class="type-consumption-factor">Seu usuário é o único vinculado a essa fazenda</p>
|
||||
{% endif %}
|
||||
{% for user in allowed_users %}
|
||||
<div class="allowed-user-card">
|
||||
<p class="name"><i class="fa-regular fa-user"></i> {{user.name}}</p>
|
||||
<a hx-get="{% url "entities:farm:farm_user_deauthorize" user.id %}" hx-swap="beforeend" hx-target="body" class="user-remove"><i class="fa-solid fa-user-slash"></i></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="available-farms-container">
|
||||
<h2 class="available-farms-title">Suas fazendas</h2>
|
||||
{% for farm in farms %}
|
||||
<div class="available-farm-card">
|
||||
<p class="available-farm-name">{{ farm.name }}</p>
|
||||
<div class="available-farm-ifo">
|
||||
<p class="animal-count">{{ farm.animal_count }} Animais</p>
|
||||
<p class="animal-count">{{ farm.paddock_count }} Piquetes</p>
|
||||
</div>
|
||||
<div class="available-farm-options">
|
||||
<a class="available-farm-select" href="{% url "entities:user:farm_set" farm.id %}">Selecionar</a>
|
||||
<a class="available-farm-delete" hx-get="{% url "entities:farm:farm_delete" farm.id %}" hx-swap="beforeend" hx-target="body">Excluir</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="user-card-container">
|
||||
{% if found_user %}
|
||||
<div class="user-card">
|
||||
<p class="name"><i class="fa-regular fa-user"></i> {{found_user.full_name}}</p>
|
||||
<p class="alert">Ao liberar esse usuário ele terá as mesmas permissões que você</p>
|
||||
<form action="{% url "entities:farm:farm_user_authorize" found_user.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit">Permitir</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if already_allowed %}
|
||||
<div class="already-allowed-user">
|
||||
<p>O usuário já possui acesso a essa fazenda</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="failed-user-find">
|
||||
<p>Não foi possível localizar esse usuário</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'layouts/base_modal.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="user-authorize-title">Autorizar usuário</h1>
|
||||
<div class="user-search">
|
||||
<label for="username">Usuário</label>
|
||||
<input type="text" id="username" name="username" placeholder="Encontre buscando pelo login do usuário" autocomplete="off">
|
||||
<button hx-get="{% url "entities:farm:get_user_card" %}" hx-include="#username" hx-target=".user-card-container" hx-swap="outterHTML">Buscar</button>
|
||||
<div class="user-card-container"></div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -27,8 +27,8 @@
|
||||
<button type="submit">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
</form>
|
||||
<a class="new-entity-button" hx-get="{% url create_url %}" hx-swap="beforeend" hx-target="body">Novo <i class="fa-solid fa-plus"></i></a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if not object_list %}
|
||||
|
||||
@@ -15,4 +15,8 @@ urlpatterns = [
|
||||
path('batch/create/', views.batch_create, name='batch_create'),
|
||||
path('batch/update/<int:batch_id>', views.batch_update, name='batch_update'),
|
||||
path('batch/delete/<int:batch_id>', views.batch_delete, name='batch_delete'),
|
||||
|
||||
path('batch/type/create/', views.batchtype_create, name='batchtype_create'),
|
||||
path('batch/type/update/<int:batchtype_id>', views.batchtype_update, name='batchtype_update'),
|
||||
path('batch/type/delete/<int:batchtype_id>', views.batchtype_delete, name='batchtype_delete'),
|
||||
]
|
||||
@@ -5,6 +5,11 @@ from ..views import PaddockListView, PaddockGroupListView
|
||||
app_name = 'farm'
|
||||
|
||||
urlpatterns = [
|
||||
path('usercard/', views.get_user_card, name='get_user_card'),
|
||||
path('authorize/', views.farm_user_authorize, name='new_authorization'),
|
||||
path('authorize/<int:user_id>', views.farm_user_authorize, name='farm_user_authorize'),
|
||||
path('deauthorize/<int:user_id>', views.farm_user_deauthorize, name='farm_user_deauthorize'),
|
||||
|
||||
path('', views.farm_home, name='farm_home'),
|
||||
path('paddock/list/', PaddockListView.as_view(), name='paddock_list'),
|
||||
path('paddock/group/list/', PaddockGroupListView.as_view(), name='paddock_group_list'),
|
||||
@@ -12,6 +17,7 @@ urlpatterns = [
|
||||
path('create/', views.farm_create, name='farm_create'),
|
||||
path('update/<int:farm_id>', views.farm_update, name='farm_update'),
|
||||
path('delete/<int:farm_id>', views.farm_delete, name='farm_delete'),
|
||||
path('consumption/update', views.consumption_update, name='consumption_update'),
|
||||
|
||||
path('paddock/create/', views.paddock_create, name='paddock_create'),
|
||||
path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'),
|
||||
|
||||
@@ -80,3 +80,39 @@ def batch_delete(request, batch_id):
|
||||
services.delete_batch(request, batch_id)
|
||||
|
||||
return redirect('entities:animal:batch_list')
|
||||
|
||||
# Batch Type views
|
||||
@farm_required
|
||||
def batchtype_create(request):
|
||||
context = services.get_batchtype_create_context(request)
|
||||
if request.method == 'GET':
|
||||
return render(request, 'batch/batchtype_create.html', context)
|
||||
|
||||
if request.method == 'POST':
|
||||
if services.create_batchtype(request):
|
||||
return redirect('entities:farm:farm_home')
|
||||
context['error'] = "Não foi possível criar o tipo de lote"
|
||||
return render(request, 'batch/batchtype_create.html', context)
|
||||
|
||||
@farm_required
|
||||
def batchtype_update(request, batchtype_id):
|
||||
context = services.get_batchtype_update_context(request, batchtype_id)
|
||||
if request.method == 'GET':
|
||||
return render(request, 'batch/batchtype_update.html', context)
|
||||
|
||||
if request.method == 'POST':
|
||||
if services.update_batchtype(request, batchtype_id):
|
||||
return redirect('entities:farm:farm_home')
|
||||
context['error'] = "Não foi possível editar o tipo de lote"
|
||||
return render(request, 'batch/batchtype_update.html', context)
|
||||
|
||||
@farm_required
|
||||
def batchtype_delete(request, batchtype_id):
|
||||
if request.method == 'GET':
|
||||
context = services.get_batchtype_delete_context(request, batchtype_id)
|
||||
return render(request, 'components/confirmation_popup.html', context)
|
||||
|
||||
if request.method == 'POST':
|
||||
services.delete_batchtype(request, batchtype_id)
|
||||
|
||||
return redirect('entities:farm:farm_home')
|
||||
@@ -17,7 +17,7 @@ def farm_create(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
if services.create_farm(request):
|
||||
return redirect('entities:farm:farm_list')
|
||||
return redirect('entities:farm:farm_home')
|
||||
context['error'] = "Não foi possível criar a fazenda"
|
||||
return render(request, 'farm/farm_create.html', context)
|
||||
|
||||
@@ -44,6 +44,11 @@ def farm_delete(request, farm_id):
|
||||
|
||||
return redirect('entities:farm:farm_create')
|
||||
|
||||
def consumption_update(request):
|
||||
if request.method=='POST':
|
||||
services.update_consumption(request)
|
||||
|
||||
return redirect('entities:farm:farm_home')
|
||||
|
||||
# Paddock Views
|
||||
@farm_required
|
||||
@@ -121,3 +126,28 @@ def paddock_group_delete(request, paddock_group_id):
|
||||
services.delete_paddock_group(request, paddock_group_id)
|
||||
|
||||
return redirect('entities:farm:paddock_group_list')
|
||||
|
||||
# User Authorization Views
|
||||
@farm_required
|
||||
def farm_user_authorize(request, user_id=None):
|
||||
if request.method == 'GET':
|
||||
return render(request, 'farm/user_authorize.html')
|
||||
if request.method == 'POST':
|
||||
if not user_id:
|
||||
return redirect('entities:farm:farm_home')
|
||||
services.authorize_user(request, user_id)
|
||||
return redirect('entities:farm:farm_home')
|
||||
|
||||
@farm_required
|
||||
def farm_user_deauthorize(request, user_id):
|
||||
if request.method == 'GET':
|
||||
context = services.get_deauthorize_user_context(request, user_id)
|
||||
return render(request, 'components/confirmation_popup.html', context)
|
||||
if request.method == 'POST':
|
||||
services.deauthorize_user(request, user_id)
|
||||
return redirect('entities:farm:farm_home')
|
||||
|
||||
@farm_required
|
||||
def get_user_card(request):
|
||||
context = services.get_user_card_context(request)
|
||||
return render(request, 'farm/user-auth-card.html', context)
|
||||
Reference in new issue
Block a user