Compare commits

..
21 Commits
Author SHA1 Message Date
Eberton 8dd945798a Salvando estado atual
Diversas modificações
2026-06-27 03:02:56 -03:00
Eberton 740547a8ed 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.
2026-05-31 01:12:30 -03:00
Eberton 8d61dc60a8 Finalizada estrutura de Cards de Movimentações abertas
A tela de listagem de movimentações agora conta com cartões que indicam, da mais antiga pra mais recente, as movimentações ainda em aberto
2026-05-31 01:03:43 -03:00
Eberton 39bd2be04a Leve organização em alguns stylesheets 2026-05-31 01:02:06 -03:00
Eberton acd8225c3d Novo JavaScript para Rotations
Criado um arquivo JavaScript com código para realizar o scroll entre os cards de Movimentações em aberto assim como proporcionar uma animação fluida para o processo.
2026-05-28 23:26:36 -03:00
Eberton 7e722c4294 Remoção de imports desnecessários
Removido imports não utilizados nos services de Animal
2026-05-28 23:25:33 -03:00
Eberton 35620936ac Fix FarmSelect
Corrigido a forma como o sistema busca as informações de Animais e Piquetes para exibir na tela de seleção de Fazenda
2026-05-28 23:24:47 -03:00
Eberton edc0f2170f Alter RotationListView
Alterado o nome do template rotation_home para rotation_list seguindo o padrão das demais ListViews e suas respectivas rotas HTTP
2026-05-28 23:24:05 -03:00
Eberton 0a814d953b Fix PaddockListView
Alterada a forma com que a informação de ocupado aparece na lista dos Piquetes, de True e False para Sim e Não
2026-05-28 23:22:58 -03:00
Eberton 287cb258a4 Reogarnização dos arquivos estáticos
Arquivos Static agora estão organizados por app e espelham a estrutura dos arquivos Template.

Alguns arquivos ainda não estão totalmente adaptados.
2026-05-28 23:21:54 -03:00
Eberton cf91224602 Cards de movimentações
1 - Modelo do Rotation agora conta com novas propriedades para calcular o tempo de movimentação.

2 - ListView da Movimentações filtra e separa rotações em aberto para serem mostradas em Card

3 - O template rotation_home inclui _active_rotation_card para exibir as informações das Movimentações em aberto
2026-05-26 22:46:46 -03:00
Eberton ed31a1a3da Organização de templates teste
Removido um template sem utilidade e alterado alguns para melhor testes.
2026-05-26 22:42:24 -03:00
Eberton e9b54535d3 Refatoração do Backend das entidades
Agora todas as entidades possuem um código de rota melhor estruturado
2026-05-26 22:41:18 -03:00
Eberton d7887019f7 Bugfix: Acesso sem selecionar fazenda
As ListViews não verificavam a existência de uma fazenda selecionada antes de exibir seu conteúdo, permitindo um acesso não intencionado
2026-05-26 22:39:46 -03:00
Eberton d3648237fb Bugfix: Links da tela dos Piquetes
Piquete utilizava links do Bloco, causando um fluxo não intencionado nas suas telas
2026-05-26 22:38:32 -03:00
Eberton e64f8078d2 Lista de entidades em Desc
Alterada a ordem de ordenação das entidades para decrescente, dessa forma os registros mais recentes se posicionam no topo da lista.
2026-05-25 23:38:14 -03:00
Eberton 04fc99bd3e Renomeações "new"->"create"
Padronizado todos os arquivos.html, rotas e funções a utilizarem o termo "create" ao invés de "new" para ação de criar um registro. Anteriormente ambos os termos eram utilizados, gerando inconsistência e confusão.
2026-05-25 23:37:33 -03:00
Eberton ae0483ddf1 Refatoração do back-end de animais e lotes
O back-end dos animais e lotes agora possui uma estrutura mais organizada e visualmente clara, é desejável  que as outras entidades sigam a mesma lógica.
2026-05-24 18:31:14 -03:00
Eberton 9374bfd71b HTMX + Modal
Implementado HTMX no projeto, o mesmo permite manipulação dinâmica de HTML por meio de requisições. O mesmo foi utilizado para implementar modais no sistema.
2026-05-24 18:30:01 -03:00
Eberton d3883669f3 Entities BaseListView
Criado uma ListView base e utilizada como padrão para exibir os registros cadastrados
2026-05-23 19:20:14 -03:00
Eberton 24650484f1 Envio do estado atual do projeto
Projeto ainda em desenvolvimento.
Enviado para o repositório para manter estado atual.
2026-03-29 15:28:16 -03:00
137 changed files with 7282 additions and 0 deletions

No files matched your search

View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'core'
@@ -0,0 +1,22 @@
from entities.models import Farm
# Injects the selected farm and the list of farms into the template context for authenticated users.
def selected_farm_processor(request):
selected_farm = None
farms = Farm.objects.none()
if request.user.is_authenticated:
farms = request.user.farms.filter(is_active=True).only("id", "name")
farm_id = request.session.get("selected_farm_id")
if farm_id:
selected_farm = farms.filter(id=farm_id).first()
if selected_farm is None:
request.session.pop("selected_farm_id", None)
return {
"SELECTED_FARM": selected_farm,
"farms": farms,
}
+18
View File
@@ -0,0 +1,18 @@
from django.shortcuts import redirect
from django.urls import reverse
from functools import wraps
FARM_SELECT_URL = 'entities:user:farm_select'
def farm_required(view_func):
"""
Decorator that ensures the user has a farm selected in the session.
If there isn't one, it redirects to the farm selection screen.
"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if 'selected_farm_id' not in request.session:
return redirect(reverse(FARM_SELECT_URL))
return view_func(request, *args, **kwargs)
return wrapper
+24
View File
@@ -0,0 +1,24 @@
from django.shortcuts import redirect
from django.conf import settings
# Forces the user to be logged in the system
# A URL whitelist can be found in the settings.py file
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.exempt_paths = getattr(settings, 'LOGIN_EXEMPT_PATHS', [])
self.exempt_prefixes = getattr(settings, 'LOGIN_EXEMPT_PREFIX', [])
def __call__(self, request):
path = request.path_info
if any(path.startswith(prefix) for prefix in self.exempt_prefixes):
return self.get_response(request)
if path in self.exempt_paths:
return self.get_response(request)
if not request.user.is_authenticated:
return redirect(settings.LOGIN_URL)
return self.get_response(request)
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
@@ -0,0 +1,61 @@
/* Fazer cada botão individual, não utilizar essa organização de botoes padroes por css único */
/* BUTTON STYLES */
.btn-simple {
color: var(--color-plus-btn);
padding: 0 var(--space-xs);
border-radius: var(--border-radius-sm);
}
.btn-simple:hover {
color: var(--color-plus-btn-hover);
background-color: var(--color-bg-dark);
transition: all var(--transition-speed) ease;
}
/* PRIMARY BUTTON (Main Action) */
.btn-primary {
background-color: var(--color-plus-btn);
color: var(--color-text-light); /* White text */
padding: var(--space-sm);
border-radius: var(--border-radius-sm);
}
.btn-primary:hover {
background-color: var(--color-plus-btn-hover); /* Darker blue on hover (a specific dark value often works best) */
}
/* SECONDARY BUTTON (Neutral Action) */
.btn-secondary {
background-color: var(--color-secondary);
color: var(--color-text-light);
padding: var(--space-sm);
border-radius: var(--border-radius-sm);
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* SUCCESS/DANGER BUTTONS */
.btn-success {
background-color: var(--color-plus-btn);
color: var(--color-text-light);
}
.btn-danger {
background-color: var(--color-danger-btn);
color: var(--color-text-light);
}
/* HOVER EFFECTS */
.highlight-hover:hover {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
transition: all var(--transition-speed) ease;
}
@@ -0,0 +1,85 @@
.confirmation-container {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 320px;
max-width: 500px;
padding: 8px;
}
/* Title */
.confirmation-container h1 {
font-size: 22px;
text-align: center;
}
/* Description */
.confirmation-description {
text-align: end;
margin: 10px 0 15px 0;
}
/* Actions */
.confirmation-container form {
display: flex;
justify-content: end;
gap: var(--space-sm);
}
/* Both buttons */
.confirmation-container form .confirm,
.confirmation-container form .negate {
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
max-height: 30px;
padding: var(--space-sm) var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
text-decoration: none;
cursor: pointer;
transition: all ease;
}
/* Confirm button */
.confirmation-container form .confirm {
background-color: var(--color-danger-btn);
color: white;
}
.confirmation-container form .confirm:hover {
background-color: var(--color-danger-btn-hover);
}
/* Negate button */
.confirmation-container form .negate {
background-color: var(--color-neutral-btn);
color: var(--color-text-light);
}
.confirmation-container form .negate:hover {
background-color: var(--color-neutral-btn-hover);
}
@media (max-width: 600px) {
.confirmation-container {
min-width: unset;
width: 100%;
padding: var(--space-md);
}
.confirmation-container form {
flex-direction: column;
}
.confirmation-container form .confirm,
.confirmation-container form .negate {
width: 100%;
}
.confirmation-container h1 {
font-size: 1.25rem;
}
}
@@ -0,0 +1,169 @@
/* Container do formulário */
.generic-form {
padding: 2rem;
background: var(--color-bg-form);
border-radius: 12px;
}
/* Labels */
.generic-form label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
color: var(--color-form-label);
}
/* Campos */
.generic-form input,
.generic-form select,
.generic-form textarea {
width: 100%;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border: 1px solid var(--color-input-border);
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
transition: all 0.2s ease;
}
/* Foco */
.generic-form input:focus,
.generic-form select:focus,
.generic-form textarea:focus {
outline: none;
border-color: var(--color-input-focus-border);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
/* Checkbox e radio */
.generic-form input[type="checkbox"],
.generic-form input[type="radio"] {
width: auto;
margin-right: 0.5rem;
}
/* Mensagens de erro */
.errorlist {
list-style: none;
padding: 0.75rem;
margin-bottom: 1rem;
background: var(--color-bg-error);
color: var(--color-text-error);
border-radius: 8px;
}
/* Texto de ajuda */
.helptext {
display: block;
margin-top: -0.5rem;
margin-bottom: 1rem;
font-size: 0.85rem;
color: var(--color-text-help);
}
/* Área dos botões */
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
/* Botão principal */
.btn-primary {
display: inline-block;
padding: 0.8rem 1.4rem;
background-color: var(--color-plus-btn);
color: var(--color-text-light); /* White text */
text-decoration: none;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: 0.2s;
}
.btn-primary:hover {
background-color: var(--color-plus-btn-hover);
}
/* Botão cancelar */
.btn-secondary {
display: inline-block;
padding: 0.8rem 1.2rem;
background-color: var(--color-neutral-btn);
color: var(--color-text-light);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: 0.2s;
}
.btn-secondary:hover {
background: var(--color-neutral-btn-hover);
}
/* Tom Select - campo */
.generic-form .ts-wrapper {
width: 100%;
margin-bottom: 1rem;
}
/* Caixa principal */
.generic-form .ts-control {
width: 100%;
min-height: 46px;
padding: 0.65rem 1rem;
border: 1px solid var(--color-input-border);
border-radius: 8px;
background: white;
box-sizing: border-box;
font-size: 1rem;
transition: all 0.2s ease;
}
/* Input interno */
.generic-form .ts-control input {
margin: 0;
padding: 0;
border: none;
box-shadow: none;
}
/* Foco */
.generic-form .ts-wrapper.focus .ts-control {
outline: none;
border-color: var(--color-input-focus-border);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
/* Tags selecionadas (multi select) */
.generic-form .item {
background: var(--color-plus-btn);
color: var(--color-text-light);
border-radius: 6px;
padding: 0.25rem 0.5rem;
}
/* Botão remover tag */
.generic-form .remove {
color: white;
}
/* Dropdown */
.generic-form .ts-dropdown {
border-radius: 8px;
border: 1px solid var(--color-input-border);
box-shadow: 0 4px 12px rgba(0,0,0,.1);
}
/* Opções */
.generic-form .option {
padding: .6rem 1rem;
}
.generic-form .option.active {
background: var(--color-plus-btn-hover);
color: var(--color-text-light);
}
@@ -0,0 +1,5 @@
.form-modal-title {
font-size: 22px;
margin-top: 10px;
margin-left: 10px;
}
@@ -0,0 +1,291 @@
/* DEFAULTS */
body {
background-color: var(--color-bg-light);
}
.w33 {
width: 33%;
}
a {
cursor: pointer;
user-select: none;
}
/* SIDE BAR */
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 250px;
background-color: var(--color-bg-sidebar);
padding: 0 var(--space-md);
overflow-y: auto;
overflow-x: hidden;
z-index: 150;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
scrollbar-width: none;
}
.sidebar-brand {
text-align: center;
padding: var(--space-md);
color: var(--color-text-light);
font-weight: bold;
font-size: var(--font-lg);
display: block;
margin-bottom: var(--space-sm);
}
.sidebar-menu {
margin: var(--space-sm) 0;
}
.sidebar-header {
color: var(--color-text-light);
font-size: var(--font-sm);
margin-top: var(--space-xs);
padding: var(--space-xs) 0;
text-transform: uppercase;
opacity: 0.6;
}
.sidebar-link {
color: var(--color-text-light);
padding: var(--space-xs) var(--space-xs);
border-radius: var(--border-radius-sm);
width: 100%;
display: block;
opacity: 1;
transition: opacity var(--transition-speed-long) cubic-bezier(0.2, 0.85, 0.75, 1.7), transform var(--transition-speed) ease;
}
.active {
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
box-shadow: inset -1px 0 0 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);
}
/* SIDEBAR TOGGLING */
body.sidebar-collapsed .sidebar {
transform: translateX(-250px);
}
body.sidebar-collapsed .sidebar-link {
opacity: -20;
transform: translateX(260px);
}
body.sidebar-collapsed .navbar,
body.sidebar-collapsed .main-content {
transform: translateX(0);
width: 100%;
}
.sidebar,
.navbar,
.main-content {
transition: all var(--transition-speed) ease;
}
/* NAV BAR */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
transform: translateX(250px);
height: var(--header-height);
width: calc(100% - 250px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--space-md);
z-index: 149;
}
#sidebar-toggle {
cursor: pointer;
}
/* Farm Selection */
.ts-wrapper {
width: clamp(200px, 25vw, 300px);
margin: 0 auto;
}
.ts-wrapper.single .ts-control {
position: relative;
width: 100%;
padding: 4px 20px 4px 6px;
border: none;
border-radius: 6px;
background: transparent;
box-shadow: none;
font-size: 16px;
justify-content: center;
cursor: pointer;
transition: none;
font-weight: 600;
color: var(--color-primary);
}
.ts-wrapper.single .ts-control:hover {
box-shadow: none;
}
.ts-wrapper.focus .ts-control {
box-shadow: none;
width: 100%;
}
.ts-wrapper.single .ts-control::after {
content: "";
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #666;
pointer-events: none;
transition: transform .2s ease;
}
.ts-wrapper.single.dropdown-active .ts-control::after {
transform: translateY(-50%) rotate(180deg);
}
.ts-dropdown {
width: 100%;
margin-top: 2px;
border: none;
border-radius: 6px;
background: #f5f5f5;
box-shadow: 2px 2px 6px rgba(0,0,0,.12);
overflow: hidden;
}
.ts-dropdown .option {
padding: 4px 6px;
font-size: 16px;
text-align: center;
cursor: pointer;
font-weight: 600;
color: var(--color-primary);
}
.ts-dropdown .option:hover {
background: #ececec;
}
.ts-dropdown .active {
background: #e0e0e0;
}
.ts-control input {
display: none;
}
.ts-control .item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ts-dropdown .option.see-all {
color: var(--color-primary);
background: transparent;
font-weight: 500;
border-top: 1px solid #ddd;
}
.ts-dropdown .option.see-all:hover,
.ts-dropdown .option.see-all.active {
cursor: pointer;
}
/* NAV BAR RIGHT */
.navbar-user-info,
.navbar-user-info-farm-select {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
}
.username {
cursor: default;
}
/* MAIN CONTENT */
.main-content {
position: absolute;
top: var(--header-height);
left: 0;
bottom: 0;
right: 0;
transform: translateX(250px);
padding: var(--space-lg);
width: calc(100% - 250px);
overflow-y: auto;
}
/* BASE MODAL */
.modal-base {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: var(--color-bg-light);
padding: var(--space-md);
border-radius: var(--space-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease;
}
@media screen and (max-width: 768px) {
body:not(.sidebar-collapsed) .navbar-farm-selector,
body:not(.sidebar-collapsed) .navbar-user-info {
display: none;
}
body:not(.sidebar-collapsed) .main-content {
transform: translateX(0);
width: 100%;
overflow: hidden;
}
.navbar {
padding: 0 var(--space-sm);
}
.w33 {
width: auto;
}
.navbar-user-info {
gap: var(--space-xs);
}
.username {
display: none;
}
}
@@ -0,0 +1,11 @@
.modal-content {
min-width: 50vw;
max-width: 85vw;
max-height: 90vh;
overflow-y: scroll;
scrollbar-width: none;
}
.modal-content:has(.confirmation-container) {
min-width: 0;
}
@@ -0,0 +1,42 @@
/*
* File: static/css/base/reset.css
* Purpose: Normalize and reset default browser styles.
*/
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
font-family: Arial, sans-serif;
line-height: 1.5;
}
ul, ol {
list-style: none;
}
a {
color: inherit;
text-decoration: none;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
@@ -0,0 +1,61 @@
:root {
/* SPECIFIC COLORS */
--login-background-primary: #244238;
--login-background-secondary: #396a5a;
--color-login-button: #396a5a;
--color-login-button-focus: #76948a;
--color-farm-select-hover: #244238;
--color-farm-select-info: #6d6d6d;
--color-new-farm-button: #396a5a;
--color-new-farm-hover: #244238;
--color-plus-btn: #396a5a;
--color-plus-btn-hover: #76948a;
--color-danger-btn: #8b3d3d;
--color-danger-btn-hover: #b37a7a;
--color-neutral-btn: #5f6b67;
--color-neutral-btn-hover: #97a39f;
--color-input-border: #5f6b67;
--color-bg-filter: #76948a;
--color-text-filter: #f5faf5;
/* GENERIC COLORS */
--color-simple-light-gray: #ccc;
--color-border-subtle: #ccc;
/* PRIMARY COLORS */
--color-primary: #244238;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #8b3d3d;
--color-danger-light: #b37a7a;
--color-warn: #dca235;
/* BACKGROUND/TEXT COLORS */
--color-bg-light: #f5f7f5;
--color-bg-white: #fafafa;
--color-bg-form: #fafafa;
--color-bg-dark: #244238;
--color-bg-sidebar: #244238;
--color-text-dark: #151422;
--color-form-label: #151422;
--color-text-light: #f8f9fa;
/* SPACING DIMENSIONS */
--space-xs: 8px;
--space-sm: 12px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--header-height: 60px;
/* FONT DIMENSIONS */
--font-xs: 12px;
--font-sm: 14px;
--font-md: 16px;
--font-lg: 24px;
--font-xl: 32px;
--border-radius-sm: 4px;
--transition-speed: 0.4s;
--transition-speed-long: 1.0s;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,51 @@
// SIDEBAR TOGGLER
document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById("sidebar-toggle");
const body = document.body;
const sidebarLinks = document.querySelectorAll(".sidebar-link");
if (!button) return;
const isMobile = window.innerWidth <= 768;
// Mobile always starts closed
if (isMobile) {
body.classList.add("sidebar-collapsed");
}
// Desktop uses the save state
else {
const savedState = localStorage.getItem("sidebarCollapsed");
if (savedState === "true") {
body.classList.add("sidebar-collapsed");
}
}
// Toggle
button.addEventListener("click", () => {
body.classList.toggle("sidebar-collapsed");
// Only saves the sidebar state on desktops
if (!isMobile) {
const isCollapsed = body.classList.contains("sidebar-collapsed");
localStorage.setItem("sidebarCollapsed", isCollapsed);
}
});
// Mobile choses sidebar when clicking a link
sidebarLinks.forEach(link => {
link.addEventListener("click", () => {
if (isMobile) {
body.classList.add("sidebar-collapsed");
}
});
});
});
// 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();
});
});
@@ -0,0 +1,33 @@
// Creates TomSelect Selects when loading a page
document.addEventListener("DOMContentLoaded", () => {
new TomSelect("#farm_selection_select", {
create: false,
sortField: {
field: "text",
direction: "asc"
},
controlInput: null,
render: {
option: function(data, escape) {
return `
<div class="option ${data.value == 0 ? 'see-all' : ''}">
${escape(data.text)}
</div>
`;
}
}
});
});
// Creates TomSelect Selects when an htmx swap is detected
document.addEventListener("htmx:afterSwap", function(event) {
const animals = document.querySelector("#id_animals");
if (animals && !animals.tomselect) {
new TomSelect("#id_animals", {
plugins: ['remove_button'],
create: false,
});
}
});
@@ -0,0 +1,16 @@
{% extends "layouts/base_modal.html" %}
{% load static %}
{% block content %}
<div class="confirmation-container">
<h1>Deseja mesmo realizar essa ação?</h1>
{% if description %}
<p class="confirmation-description">{{ description }}</p>
{% endif %}
<form method="POST" action="{{ positive_url }}">
{% csrf_token %}
<button class="confirm" type="submit">Sim</button>
<a href="{{ negative_url }}" class="negate">Não</a>
</form>
</div>
{% endblock content %}
@@ -0,0 +1,30 @@
<form method="POST" action="{{ action_url }}" class="generic-form">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<small class="helptext">{{ field.help_text }}</small>
{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<div class="form-actions">
<button class="btn-primary" type="submit">
{% if action_button_text %}
{{ action_button_text }}
{% else %}
Salvar
{% endif %}
</button>
<a href="{{ return_url }}" class="btn-secondary">
Cancelar
</a>
</div>
</form>
@@ -0,0 +1,7 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<h1 class="form-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,24 @@
<nav class="navbar">
<div class="w33">
<i class="fa-solid fa-bars" id="sidebar-toggle"></i>
</div>
<form method="post" action="{% url 'entities:user:farm_set_post' %}" class="navbar-farm-selector w33">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<select name="farm_id" id="farm_selection_select" onchange="this.form.submit()">
<option value="0" class="see-all">Ver todas</option>
{% for farm in farms %}
<option value="{{ farm.id }}" {% if farm.id == SELECTED_FARM.id %}selected{% endif %}>
{{ farm.name|upper }}
</option>
{% endfor %}
</select>
</form>
<div class="navbar-user-info w33">
<span class="username">Olá, {{ request.user.short_name }}</span>
<a href="{% url 'entities:user:farm_deselect' %}"><i class="fa-solid fa-user-pen"></i></a>
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
</div>
</nav>
@@ -0,0 +1,50 @@
<aside class="sidebar">
<div class="sidebar-brand">
<a href="{% url 'entities:farm:farm_home' %}">MultiFarm</a>
</div>
<ul class="sidebar-menu">
<li class="sidebar-header">ACOMPANHAMENTO</li>
<li class="{% if request.resolver_match.url_name != 'report' %}highlight-hover{% endif %}">
<a href="{% url 'report:dashboard' %}" class="sidebar-link {% if request.resolver_match.url_name == 'report' %}active{% endif %}"><i class="fa-solid fa-chart-pie"></i> Painel</a>
</li>
<li class="sidebar-header">REGISTROS</li>
<li class="{% if request.resolver_match.url_name not in 'rotation_home, rotation_create, rotation_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:rotation:rotation_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'rotation_home, rotation_create, rotation_delete' %}active{% endif %}"><i class="fa-solid fa-arrows-spin"></i> Movimentações</a>
</li>
<li class="sidebar-header">CADASTROS</li>
<li class="{% if request.resolver_match.url_name not in 'animal_list, animal_create, animal_update, animal_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:animal:animal_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'animal_list, animal_create, animal_update, animal_delete' %}active{% endif %}"><i class="fa-solid fa-cow"></i> Animais</a>
</li>
<li class="{% if request.resolver_match.url_name not in 'batch_list, batch_create, batch_update, batch_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:animal:batch_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'batch_list, batch_create, batch_update, batch_delete' %}active{% endif %}"><i class="fa-solid fa-layer-group"></i> Lotes</a>
</li>
<li class="{% if request.resolver_match.url_name not in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}active{% endif %}"><i class="fa-solid fa-seedling"></i> Piquetes</a>
</li>
<!--
<li class="{% if request.resolver_match.url_name not in 'paddock_group_list, paddock_group_create, paddock_group_update, paddock_group_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_group_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_group_list, paddock_group_create, paddock_group_update, paddock_group_delete' %}active{% endif %}"><i class="fa-solid fa-layer-group"></i> Blocos</a>
</li>
-->
<li class="sidebar-header">CONFIGURAÇÔES</li>
<li class="{% if request.resolver_match.url_name not in 'farm_home, farm_create, farm_update, farm_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:farm_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'farm_home, farm_create, farm_update, farm_delete' %}active{% endif %}"><i class="fa-solid fa-gear"></i> Fazenda</a>
</li>
{% if user.is_staff or user.is_superuser %}
<li class="{% if request.resolver_match.url_name not in 'admin' %}highlight-hover{% endif %}">
<a href="{% url 'admin:index' %}" target="_blank" class="sidebar-link"><i class="fa-solid fa-user-tie"></i> Painel Administrativo</a>
</li>
{% endif %}
</ul>
</aside>
@@ -0,0 +1,51 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="shortcut icon" href="{% static 'core/img/favicon/favicon.ico' %}" type="image/x-icon">
<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/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/layouts/base_modal.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form_modal.css' %}">
<link rel="stylesheet" href="{% static 'report/css/request_report.css' %}">
<link rel="stylesheet" href="{% static 'rotation/css/rotation_view_more.css' %}">
{% block extra_css %}{% endblock %}
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Importing Tom Select -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/css/tom-select.css" rel="stylesheet">
</head>
<body>
{% include 'components/sidebar.html' %}
{% include 'components/navbar.html' %}
<div class="main-content">
{% block content %}{% endblock %}
</div>
</body>
<!-- Importing custom JS -->
<script src="{% static 'core/js/main.js' %}"></script>
<script src="{% static 'core/js/tomselect.js' %}"></script>
<!-- Importing HTMX -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
<!-- Importing Tom Select -->
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/js/tom-select.complete.min.js"></script>
{% block extra_js %}{% endblock extra_js %}
</html>
@@ -0,0 +1,9 @@
<div class="modal-base" hx-get="{% url 'core:null' %}" hx-swap="delete" hx-target=".modal-base">
<div class="modal-content"
onclick="event.stopPropagation()">
{% block content %}{% endblock %}
{% if error %}
<div class="modal-error">{{error}}</div>
{% endif %}
</div>
</div>
@@ -0,0 +1 @@
404 not found
@@ -0,0 +1,71 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seleção de Fazenda</title>
<link rel="shortcut icon" href="{% static 'core/img/favicon/favicon.ico' %}" type="image/x-icon">
<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 'user/css/farm_select.css' %}">
<!-- Templates based on base_model can't pass their stylesheet on block extra_css -->
<link rel="stylesheet" href="{% static 'core/css/layouts/base_modal.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form_modal.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{% block extra_css %}{% endblock extra_css %}
</head>
<body>
<nav class="farm-select-navbar">
<div class="navbar-brand">
MultiFarm
</div>
<div class="navbar-user-info-farm-select">
<span class="username">Olá, {{ request.user.short_name }}</span>
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
</div>
</nav>
<div class="farm-select-container">
<h1>Selecione sua fazenda</h1>
{% if farms %}
<ul class="farm-list">
{% for farm in farms %}
<li>
<a href="{% url 'entities:user:farm_set' farm_id=farm.id %}" class="farm-button">
<div class="farm-item">
<h3 class="farm-name">{{ farm.name }}</h3>
<p class="farm-description">
<span class="count-piquetes">Piquetes: {{ farm.total_paddocks }}</span>
<span class="count-animais">Animais: {{ farm.total_animals }}</span>
</p>
</div>
</a>
</li>
{% endfor %}
<p><a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body" class="new-farm-btn"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
</ul>
{% else %}
<div class="no-farms">
<p>Nenhuma fazenda encontrada</p>
<p><a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body" class="new-farm-btn"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
</div>
{% endif %}
</div>
</body>
<!-- Importing HTMX -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
</html>
@@ -0,0 +1,41 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link rel="shortcut icon" href="{% static 'core/img/favicon/favicon.ico' %}" type="image/x-icon">
<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 'user/css/login.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div class="login-container">
<div class="ilustration-container">
<h1>Multifarm pastoreio</h1>
</div>
<div class="login-form-container">
<h1>Login</h1>
<form method="POST" action="{% url 'entities:user:user_login' %}">
{% csrf_token %}
{{ form }}
{% if fail_login %}
<p class="login-fail"><i class="fa-solid fa-circle-exclamation"></i> Usuário ou senha incorretos</p>
{% endif %}
<button type="submit">Entrar <i class="fa-solid fa-right-to-bracket"></i></button>
<p>Não possui cadastro? <a href="{% url 'entities:user:user_signin' %}">Solicite acesso</a>.</p>
</form>
</div>
</div>
</body>
</html>
Whitespace-only changes.
@@ -0,0 +1,22 @@
from django import template
register = template.Library()
"""
Allows dinamic access to object attributes in templates.
Exemple:
{{ obj|get_attr:"name" }}
"""
@register.filter
def get_attr(obj, attr):
try:
value = getattr(obj, attr)
if callable(value):
value = value()
# If none, return empty string to avoid displaying "None" in the template
return value if value is not None else "-"
except Exception:
return "- !"
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+8
View File
@@ -0,0 +1,8 @@
from django.urls import include, path
from . import views
app_name = "core"
urlpatterns = [
path("null/", views.null_response, name="null"),
]
+4
View File
@@ -0,0 +1,4 @@
from django.http import HttpResponse
def null_response(request):
return HttpResponse('<div class="null"></div>')
Whitespace-only changes.
+34
View File
@@ -0,0 +1,34 @@
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models.user import User
from .models.farm import Farm, PaddockGroup, Paddock
from .models.animal import Animal, BatchType, Batch, BatchAnimal, FeedingRecord
from .models.rotation import Rotation
from .models import (
User,
Farm,
PaddockGroup,
Paddock,
Animal,
Batch,
BatchAnimal,
FeedingRecord,
Rotation,
BatchType,
)
class DefaultImportExportAdmin(ImportExportModelAdmin):
pass
admin.site.register(User, DefaultImportExportAdmin)
admin.site.register(Farm, DefaultImportExportAdmin)
admin.site.register(PaddockGroup, DefaultImportExportAdmin)
admin.site.register(Paddock, DefaultImportExportAdmin)
admin.site.register(Animal, DefaultImportExportAdmin)
admin.site.register(Batch, DefaultImportExportAdmin)
admin.site.register(BatchAnimal, DefaultImportExportAdmin)
admin.site.register(FeedingRecord, DefaultImportExportAdmin)
admin.site.register(Rotation, DefaultImportExportAdmin)
admin.site.register(BatchType, DefaultImportExportAdmin)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class EntitiesConfig(AppConfig):
name = 'entities'
@@ -0,0 +1,4 @@
from .animal import *
from .farm import *
from .rotation import *
from .user import *
+56
View File
@@ -0,0 +1,56 @@
from django import forms
from ..models.animal import Batch, Animal, BatchType
class AnimalCreateForm(forms.ModelForm):
class Meta:
model = Animal
fields = ['identifier', 'birth', 'weight']
labels = {
'identifier': 'Identificador',
'birth': 'Data de Nascimento',
'weight': 'Peso (kg)',
}
widgets = {
'birth': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'weight': forms.NumberInput(attrs={'step': '0.01'})
}
class AnimalEditForm(forms.ModelForm):
class Meta:
model = Animal
fields = ['identifier', 'weight']
labels = {
'identifier': 'Identificador',
'weight': 'Peso (kg)',
}
widgets = {
'weight': forms.NumberInput(attrs={'step': '0.01'})
}
class BatchForm(forms.ModelForm):
class Meta:
model = Batch
fields = ['identifier', 'animal_category', 'batch_type', 'animals']
labels = {
'identifier': 'Identificador',
'animal_category': 'Categoria animal',
'batch_type': 'Tipo',
'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
+36
View File
@@ -0,0 +1,36 @@
from django import forms
from ..models import Farm, PaddockGroup, Paddock
class FarmForm(forms.ModelForm):
class Meta:
model = Farm
fields = ['name']
labels = {
'name': 'Nome',
}
FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
class PaddockGroupForm(forms.ModelForm):
class Meta:
model = PaddockGroup
fields = ['identifier', 'description']
labels = {
'identifier': 'Identificador',
'description': 'Descrição',
}
PaddockGroupCreateForm = PaddockGroupEditForm = PaddockGroupForm
class PaddockForm(forms.ModelForm):
class Meta:
model = Paddock
fields = ['paddock_number', 'area'] #, 'load_capacity', 'paddock_group']
labels = {
'paddock_number': 'Número do piquete',
'area': 'Área (m²)',
# 'load_capacity': 'Lotação máxima',
# 'paddock_group': 'Bloco',
}
PaddockCreateForm = PaddockEditForm = PaddockForm
@@ -0,0 +1,23 @@
from django import forms
from ..models import Rotation
class RotationEntryForm(forms.ModelForm):
class Meta:
model = Rotation
fields = ['batch', 'paddock', 'entry_date']
labels = {
'batch': 'Lote de animais',
'paddock': 'Piquete',
'entry_date': 'Horário da entrada'
}
widgets = {
'entry_date': forms.DateTimeInput(
attrs={
'type': 'datetime-local',
'class': 'form-control'
},
format='%Y-%m-%dT%H:%M'
),
}
+35
View File
@@ -0,0 +1,35 @@
from django import forms
from ..models import User
class UserCreateForm(forms.ModelForm):
class Meta:
model = User
fields = ['name', 'login', 'email',]
labels = {
'name': 'Nome',
'login': 'Login',
'email': 'E-mail',
}
widgets = {
'email': forms.EmailInput(),
}
class UserEditForm(forms.ModelForm):
class Meta:
model = User
fields = ['name', 'email',]
labels = {
'name': 'Nome',
'email': 'E-mail',
}
widgets = {
'email': forms.EmailInput(),
}
class UserLoginForm(forms.Form):
login = forms.CharField(max_length=45, label='Usuário')
password = forms.CharField(widget=forms.PasswordInput, label='Senha')
@@ -0,0 +1,145 @@
# Generated by Django 6.0.2 on 2026-02-28 19:45
import django.db.models.deletion
import django.utils.timezone
import entities.models.user
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Animal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('birth', models.DateField()),
('weight', models.DecimalField(decimal_places=2, max_digits=10)),
],
),
migrations.CreateModel(
name='Batch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('animal_category', models.CharField(blank=True, max_length=45, null=True)),
('batch_type', models.CharField(blank=True, max_length=45, null=True)),
],
),
migrations.CreateModel(
name='Farm',
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)),
('total_area', models.IntegerField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='PaddockGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('description', models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='BatchAnimal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.animal')),
('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.batch')),
],
),
migrations.AddField(
model_name='batch',
name='animals',
field=models.ManyToManyField(related_name='batches', through='entities.BatchAnimal', to='entities.animal'),
),
migrations.AddField(
model_name='animal',
name='farm',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='animals', to='entities.farm'),
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('name', models.CharField(max_length=45)),
('login', models.CharField(max_length=45, unique=True)),
('email', models.EmailField(max_length=254, unique=True)),
('is_active', models.BooleanField(default=True)),
('is_superuser', models.BooleanField(default=False)),
('is_staff', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('last_login', models.DateTimeField(blank=True, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
('farms', models.ManyToManyField(related_name='users', to='entities.farm')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
},
managers=[
('objects', entities.models.user.CustomUserManager()),
],
),
migrations.CreateModel(
name='Paddock',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('paddock_number', models.IntegerField()),
('area', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('load_capacity', models.IntegerField(blank=True, null=True)),
('occupied', models.BooleanField(default=False)),
('farm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paddocks', to='entities.farm')),
('paddock_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paddocks', to='entities.paddockgroup')),
],
),
migrations.CreateModel(
name='Rotation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('entry_date', models.DateTimeField()),
('entry_height', models.DecimalField(decimal_places=2, max_digits=10)),
('exit_date', models.DateTimeField(blank=True, null=True)),
('exit_height', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('animal_count', models.IntegerField()),
('livestock_unit', models.DecimalField(decimal_places=2, max_digits=10)),
('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rotations', to='entities.batch')),
('paddock', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rotations', to='entities.paddock')),
],
),
migrations.CreateModel(
name='FeedingRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feed_quantity', models.DecimalField(decimal_places=2, max_digits=10)),
('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.animal')),
('rotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.rotation')),
],
),
migrations.AddConstraint(
model_name='batchanimal',
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('animal',), name='unique_active_animal_batch_link'),
),
migrations.AddConstraint(
model_name='animal',
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('identifier', 'farm'), name='unique_active_farm_animal'),
),
]
@@ -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'),
),
]
@@ -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),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 6.0.2 on 2026-06-08 16:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0003_farm_default_consumption_farm_summer_consumption_and_more'),
]
operations = [
migrations.AddField(
model_name='farm',
name='ideal_paddock_occupation',
field=models.IntegerField(default=2),
),
migrations.AddField(
model_name='farm',
name='ideal_paddock_rest',
field=models.IntegerField(default=20),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-06-10 15:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0004_farm_ideal_paddock_occupation_and_more'),
]
operations = [
migrations.AlterField(
model_name='farm',
name='default_consumption',
field=models.DecimalField(decimal_places=2, default=2.5, max_digits=4),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.2 on 2026-06-21 20:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('entities', '0005_alter_farm_default_consumption'),
]
operations = [
migrations.RemoveField(
model_name='farm',
name='total_area',
),
]
@@ -0,0 +1,19 @@
# Generated by Django 6.0.2 on 2026-06-23 00:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0006_remove_farm_total_area'),
]
operations = [
migrations.AlterField(
model_name='rotation',
name='batch',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rotations', to='entities.batch'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-06-23 00:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0007_alter_rotation_batch'),
]
operations = [
migrations.AlterField(
model_name='farm',
name='default_consumption',
field=models.DecimalField(decimal_places=2, default=2.75, max_digits=4),
),
]
Whitespace-only changes.
@@ -0,0 +1,4 @@
from .user import *
from .farm import *
from .animal import *
from .rotation import *
+153
View File
@@ -0,0 +1,153 @@
from datetime import date
from django.db import models
from django.db.models import Q, Sum
from decimal import Decimal
class Animal(models.Model):
is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45)
birth = models.DateField()
weight = models.DecimalField(max_digits=10, decimal_places=2)
farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='animals')
def __str__(self):
return f"{self.identifier}"
@property
def age(self):
today = date.today()
birth_date = self.birth
years = today.year - birth_date.year
months = today.month - birth_date.month
if today.day < birth_date.day:
months -= 1
if months < 0:
months += 12
years -= 1
if years > 0 and months > 0:
return f"{years} anos e {months} meses" # YYYY years and MM months
elif years > 0:
return f"{years} anos" # YYYY years
elif months > 0:
return f"{months} meses" # MM months
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(
fields=['identifier', 'farm'],
condition=Q(is_active=True),
name='unique_active_farm_animal'
)
]
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):
return f"{self.name} ({self.consumption_percentage}%)"
@property
def consumption_percentage(self):
value = int((self.consumption_factor - 1) * 100)
if value >= 0:
return f'+{value}'
return f'{value}'
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.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):
super().__init__(*args, **kwargs)
self.__original_is_active = self.is_active
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
is_active_changed = self.__original_is_active != self.is_active
if is_active_changed:
if self.is_active is False:
self.batchanimal_set.all().update(is_active=False)
elif self.is_active is True:
self.batchanimal_set.all().update(is_active=True)
self.__original_is_active = self.is_active
@property
def total_ugm(self):
if self.animals:
total_weight = self.animals.aggregate(
total=Sum("weight")
)["total"] or 0
return Decimal(f"{total_weight / 500:.2f}")
def __str__(self):
return self.identifier
class BatchAnimal(models.Model):
animal = models.ForeignKey(Animal, on_delete=models.CASCADE)
batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
is_active = models.BooleanField(default=True)
# When reactivating a batch, all its animals must not belong to any other active batch.
# Otherwise, the database will raise an IntegrityError due to the unique constraint on active animals.
# This is a rare use case, but any future code regarding it must implement error treatment
class Meta:
constraints = [
models.UniqueConstraint(
fields=['animal'],
condition=Q(is_active=True),
name='unique_active_animal_batch_link'
)
]
def __str__(self):
return f"{self.animal.identifier} in {self.batch.identifier}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_is_active = self.is_active
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)
self.__original_is_active = self.is_active
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')
feed_quantity = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"{self.animal.identifier} - {self.feed_quantity}kg"
+88
View File
@@ -0,0 +1,88 @@
from django.db import models
from django.db.models import Sum
from django.apps import apps
from django.utils import timezone
class Farm(models.Model):
is_active = models.BooleanField(default=True)
name = models.CharField(max_length=45)
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=2.75)
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)
ideal_paddock_occupation = models.IntegerField(default=2)
ideal_paddock_rest = models.IntegerField(default=20)
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()
@property
def total_ugm(self):
total_weight = (
self.animals
.filter(is_active=True)
.aggregate(total=Sum('weight'))['total']
or 0
)
return round(total_weight / 500, 2)
@property
def batch_count(self):
Batch = apps.get_model('entities', 'Batch')
return Batch.objects.filter(
animals__farm=self,
is_active=True
).distinct().count()
@property
def paddocks_area(self):
return sum(paddock.area for paddock in self.paddocks.filter(is_active=True))
@property
def total_rotations(self):
Rotation = apps.get_model('entities', 'Rotation')
return Rotation.objects.filter(
is_active=True,
paddock__farm=self
).count()
class PaddockGroup(models.Model):
is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45)
description = models.TextField(null=True, blank=True)
def __str__(self):
return f"{self.id}"
class Paddock(models.Model):
is_active = models.BooleanField(default=True)
paddock_number = models.IntegerField()
area = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
load_capacity = models.IntegerField(null=True, blank=True)
occupied = models.BooleanField(default=False)
farm = models.ForeignKey(Farm, on_delete=models.CASCADE, related_name='paddocks')
paddock_group = models.ForeignKey(PaddockGroup, on_delete=models.SET_NULL, related_name='paddocks', null=True, blank=True)
def __str__(self):
return f"{self.paddock_number}"
@property
def occupied_as_text(self):
if self.occupied:
return "Sim"
return "Não"
# @property
# def is_occupied(self):
# from . import Rotation
# return Rotation.objects.filter(paddock=self, is_active=True).exists()
@@ -0,0 +1,59 @@
from django.db import models
from django.utils import timezone
from decimal import Decimal
class Rotation(models.Model):
is_active = models.BooleanField(default=True)
entry_date = models.DateTimeField()
entry_height = models.DecimalField(max_digits=10, decimal_places=2)
exit_date = models.DateTimeField(null=True, blank=True)
exit_height = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
animal_count = models.IntegerField()
livestock_unit = models.DecimalField(max_digits=10, decimal_places=2)
batch = models.ForeignKey('entities.Batch', on_delete=models.PROTECT, related_name='rotations', null=True, blank=True)
paddock = models.ForeignKey('entities.Paddock', on_delete=models.CASCADE, related_name='rotations')
def __str__(self):
return f"Movimentação {self.id}"
@property
def duration(self):
end = self.exit_date or timezone.now()
delta = end - self.entry_date
return Decimal(str(round(delta.total_seconds() / 86400, 2)))
@property
def duration_display_day(self):
days = self.duration
if days < 1:
return "menos de um"
string_day = f"{days}".replace('.', ',')
return string_day
@property
def livestock_unit_day(self):
if self.duration == 0:
return 0
return Decimal(self.livestock_unit * self.duration)
@property
def animal_count_day(self):
if self.duration == 0:
return 0
return Decimal(self.animal_count * self.duration)
@property
def season(self):
month = self.exit_date.month
day = self.exit_date.day
if (month == 12 and day >= 21) or (month in [1, 2]) or (month == 3 and day < 20):
return "Summer"
elif (month == 3 and day >= 20) or (month in [4, 5]) or (month == 6 and day < 21):
return "Fall"
elif (month == 6 and day >= 21) or (month in [7, 8]) or (month == 9 and day < 22):
return "Winter"
else:
return "Spring"
+63
View File
@@ -0,0 +1,63 @@
from django.db import models
from django.contrib.auth.models import UserManager, AbstractBaseUser, PermissionsMixin
from django.utils import timezone
class CustomUserManager(UserManager):
def _create_user(self, name, login, email, password, **extra_fields):
if not login:
raise ValueError("Information provided is not a valid login")
if not email:
raise ValueError("You must inform an e-mail adress")
email = self.normalize_email(email)
user = self.model(
name = name,
login = login,
email = email,
**extra_fields)
user.set_password(password)
user.save(using=self.db)
return user
def create_user(self, name=None, login=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(name, login, email, password, **extra_fields)
def create_superuser(self, name=None, login=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self._create_user(name, login, email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
name = models.CharField(max_length=45)
login = models.CharField(max_length=45, unique=True)
email = models.EmailField(unique=True)
farms = models.ManyToManyField('entities.Farm', related_name='users')
is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now)
last_login = models.DateTimeField(blank=True, null=True)
objects = CustomUserManager()
USERNAME_FIELD = 'login'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'email']
class Meta:
verbose_name = 'User'
verbose_name_plural = 'Users'
@property
def full_name(self):
return self.name
@property
def short_name(self):
return self.name.split(' ')[0] or self.email.split('@')[0]
Whitespace-only changes.
@@ -0,0 +1,268 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from ..models import Animal, Batch, BatchType
from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm, BatchTypeCreateForm, BatchTypeEditForm
from entities.models import Farm
# Local utility functions
def _get_selected_farm_id(request):
return request.session['selected_farm_id']
def _get_selected_farm(request):
return Farm.objects.get(id=_get_selected_farm_id(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_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()
context = {}
context['title'] = 'Novo animal'
context['form'] = form
context['action_url'] = reverse('entities:animal:animal_create')
context['return_url'] = reverse('entities:animal:animal_list')
context['modal_title'] = 'Cadastrar animal'
return context
def create_animal(request):
form = AnimalCreateForm(request.POST)
selected_farm = _get_selected_farm(request)
if form.is_valid():
animal = form.save(commit=False)
animal.farm = selected_farm
animal.save()
return True
return False
def get_animal_update_context(request, animal_id):
animal = _get_animal_by_id(request, animal_id)
form = AnimalEditForm(instance=animal)
context = {}
context['title'] = 'Editar animal'
context['action_url'] = reverse(
'entities:animal:animal_update',
kwargs={'animal_id': animal.pk}
)
context['return_url'] = reverse('entities:animal:animal_list')
context['form'] = form
context['modal_title'] = 'Editar animal'
return context
def update_animal(request, animal_id):
animal = _get_animal_by_id(request, animal_id)
form = AnimalEditForm(request.POST, instance=animal)
if form.is_valid():
form.save()
return True
return False
def get_animal_delete_context(request, animal_id):
animal = _get_animal_by_id(request, animal_id)
context = {}
context['description'] = f"Excluir registro de animal {animal.identifier}"
context['positive_url'] = reverse('entities:animal:animal_delete', kwargs={'animal_id': animal_id})
context['negative_url'] = reverse('entities:animal:animal_list')
return context
def delete_animal(request, animal_id):
animal = _get_animal_by_id(request, animal_id)
animal.is_active = False
animal.save()
# Batch Services
def get_batch_create_context(request):
form = BatchForm()
form.fields['animals'].queryset = _get_no_batch_farms_animals(request)
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
context = {
'title': 'Novo Lote',
'form': form,
'action_url': reverse('entities:animal:batch_create'),
'return_url': reverse('entities:animal:batch_list'),
'modal_title': 'Cadastrar lote'
}
return context
def create_batch(request):
form = BatchForm(request.POST)
if form.is_valid():
batch = form.save(commit=False)
batch.save()
form.save_m2m()
return True
return False
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 = animals_querset
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
context = {
'title': f'Editar Lote {batch_old.identifier}',
'return_url': reverse('entities:animal:batch_list'),
'action_url': reverse(
'entities:animal:batch_update',
kwargs={'batch_id': batch_old.pk}
),
'form': form,
'modal_title': 'Editar lote'
}
return context
def update_batch(request, batch_id):
form = BatchForm(request.POST)
batch_old = _get_batch_by_id(request, batch_id)
if form.is_valid():
batch_old.is_active = False
batch_old.save()
batch_new = form.save(commit=False)
batch_new.is_active = True
batch_new.save()
form.instance = batch_new
form.save_m2m()
return True
return False
def get_batch_delete_context(request, batch_id):
batch = _get_batch_by_id(request, batch_id)
context = {}
context['description'] = f"Excluir registro do lote {batch.identifier}"
context['positive_url'] = reverse('entities:animal:batch_delete', kwargs={'batch_id': batch_id})
context['negative_url'] = reverse('entities:animal:batch_list')
return context
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()
+344
View File
@@ -0,0 +1,344 @@
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, BatchType, User
from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm
def _get_selected_farm_id(request):
return request.session['selected_farm_id']
def _get_selected_farm(request):
return get_object_or_404(
Farm,
pk=_get_selected_farm_id(request),
is_active=True
)
def _get_farm_by_id(request, farm_id):
return get_object_or_404(
Farm,
pk=farm_id,
is_active=True
)
def _get_paddock_by_id(request, paddock_id):
return get_object_or_404(
Paddock,
pk=paddock_id,
farm_id=_get_selected_farm_id(request),
is_active=True
)
def _get_active_farm_paddocks(request):
return Paddock.objects.filter(
farm=_get_selected_farm(request),
is_active=True
)
def _get_paddock_group_by_id(request, paddock_group_id):
return get_object_or_404(
PaddockGroup,
pk=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'] = 'Configurações da Fazenda'
context['selected_farm'] = selected_farm
context['farms'] = farms_with_counts
context['allowed_users'] = allowed_users
context['batch_types'] = batch_types
return context
def get_farm_create_context():
form = FarmCreateForm()
context = {}
context['title'] = 'Nova fazenda'
context['form'] = form
context['action_url'] = reverse('entities:farm:farm_create')
context['return_url'] = reverse('entities:farm:farm_home')
context['modal_title'] = 'Criar fazenda'
return context
def create_farm(request):
form = FarmCreateForm(request.POST)
authenticated_user = request.user
if form.is_valid():
farm = form.save()
farm.users.add(authenticated_user)
return True
return False
def get_update_farm_context(request, farm_id):
farm = _get_farm_by_id(request, farm_id)
form = FarmEditForm(instance=farm)
context = {}
context['title'] = 'Editar fazenda'
context['action_url'] = reverse(
'entities:farm:farm_update',
kwargs={'farm_id': farm.pk}
)
context['return_url'] = reverse('entities:farm:farm_home')
context['form'] = form
return context
def update_farm(request, farm_id):
farm = _get_farm_by_id(request, farm_id)
form = FarmEditForm(request.POST, instance=farm)
if form.is_valid():
form.save()
return True
return False
def get_farm_delete_context(request, farm_id):
farm = _get_farm_by_id(request, farm_id)
context = {}
context['description'] = f"Excluir registro da fazenda {farm.name}"
context['positive_url'] = reverse(
'entities:farm:farm_delete',
kwargs={'farm_id': farm_id}
)
context['negative_url'] = reverse('entities:farm:farm_home')
return context
def delete_farm(request, farm_id):
farm = _get_farm_by_id(request, farm_id)
farm.is_active = False
farm.save()
def update_metrics(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'))
ideal_paddock_occupation = int(request.POST.get('ideal_paddock_occupation'))
ideal_paddock_rest = int(request.POST.get('ideal_paddock_rest'))
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.ideal_paddock_occupation = ideal_paddock_occupation
selected_farm.ideal_paddock_rest = ideal_paddock_rest
selected_farm.save()
# Paddock Services
def get_paddock_create_context():
form = PaddockCreateForm()
context = {
'title': 'Novo Piquete',
'form': form,
'action_url': reverse('entities:farm:paddock_create'),
'return_url': reverse('entities:farm:paddock_list')
}
return context
def create_paddock(request):
form = PaddockCreateForm(request.POST)
selected_farm = _get_selected_farm(request)
if form.is_valid():
paddock = form.save(commit=False)
paddock.farm = selected_farm
paddock.save()
return True
return False
def get_paddock_update_context(request, paddock_id):
paddock = _get_paddock_by_id(request, paddock_id)
form = PaddockEditForm(instance=paddock)
context = {
'title': 'Editar piquete',
'action_url': reverse(
'entities:farm:paddock_update',
kwargs={'paddock_id': paddock.pk}
),
'return_url': reverse('entities:farm:paddock_list'),
'form': form
}
return context
def update_paddock(request, paddock_id):
paddock = _get_paddock_by_id(request, paddock_id)
form = PaddockEditForm(request.POST, instance=paddock)
if form.is_valid():
form.save()
return True
return False
def get_paddock_delete_context(request, paddock_id):
paddock = _get_paddock_by_id(request, paddock_id)
context = {}
context['description'] = f"Excluir registro do piquete {paddock.identifier}"
context['positive_url'] = reverse(
'entities:farm:paddock_delete',
kwargs={'paddock_id': paddock_id}
)
context['negative_url'] = reverse('entities:farm:paddock_list')
return context
def delete_paddock(request, paddock_id):
paddock = _get_paddock_by_id(request, paddock_id)
paddock.is_active = False
paddock.save()
# Paddock Group Services
def get_paddock_group_create_context(request):
form = PaddockGroupCreateForm()
context = {
'title': 'Novo Bloco',
'form': form,
'action_url': reverse('entities:farm:paddock_group_create'),
'return_url': reverse('entities:farm:paddock_group_list')
}
return context
def create_paddock_group(request):
form = PaddockGroupCreateForm(request.POST)
if form.is_valid():
form.save()
return True
return False
def get_paddock_group_update_context(request, paddock_group_id):
paddock_group = _get_paddock_group_by_id(request, paddock_group_id)
form = PaddockGroupEditForm(instance=paddock_group)
context = {
'title': f'Editar Bloco {paddock_group.identifier}',
'action_url': reverse(
'entities:farm:paddock_group_update',
kwargs={'paddock_group_id': paddock_group.pk}
),
'return_url': reverse('entities:farm:paddock_group_list'),
'form': form
}
return context
def update_paddock_group(request, paddock_group_id):
paddock_group = _get_paddock_group_by_id(request, paddock_group_id)
form = PaddockGroupEditForm(
request.POST,
instance=paddock_group
)
if form.is_valid():
form.save()
return True
return False
def get_paddock_group_delete_context(request, paddock_group_id):
paddock_group = _get_paddock_group_by_id(request, paddock_group_id)
context = {}
context['description'] = (
f"Excluir registro do bloco {paddock_group.identifier}"
)
context['positive_url'] = reverse(
'entities:farm:paddock_group_delete',
kwargs={'paddock_group_id': paddock_group_id}
)
context['negative_url'] = reverse(
'entities:farm:paddock_group_list'
)
return context
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,73 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.db.models import Max, Q, F, Exists, OuterRef
from django.utils.safestring import mark_safe
from django.utils import timezone
from entities.models import Farm, Rotation, Paddock, Batch
from entities.forms import RotationEntryForm
from report.services import _get_rotation_consumption
def _get_selected_farm_id(request):
return request.session['selected_farm_id']
def _get_selected_farm(request):
return Farm.objects.get(id=_get_selected_farm_id(request))
def _get_rotation_by_id(request, rotation_id):
farm = _get_selected_farm(request)
return get_object_or_404(Rotation, pk=rotation_id, paddock__farm=farm)
def _get_batches(farm):
return (Batch.objects
.filter(animals__farm=farm, is_active=True)
.annotate(
in_rotation=Exists(
Rotation.objects.filter(
batch=OuterRef("pk"),
exit_date__isnull=True
)
)
)
.distinct()
)
def _get_free_paddocks(farm):
now = timezone.now()
paddocks = (
Paddock.objects
.filter(farm=farm, occupied=False)
.annotate(last_exit=Max("rotations__exit_date"))
.order_by(F("last_exit").asc(nulls_first=True))
)
for paddock in paddocks:
paddock.current_rest = (now - paddock.last_exit).days if paddock.last_exit else None
return paddocks
def get_rotation_create_context(request):
farm = _get_selected_farm(request)
form = RotationEntryForm()
form.fields['batch'].queryset = _get_batches(farm)
form.fields['batch'].empty_label = "Selecione o lote"
form.fields['paddock'].queryset = _get_free_paddocks(farm)
form.fields['paddock'].empty_label = "Selecione o piquete"
return {
'form': form,
'modal_title': 'Iniciar movimentação',
'action_button_text': mark_safe("""<i class="fa-solid fa-arrow-rotate-right"></i> Movimentar""")
}
def get_view_more_context(request, rotation_id):
rotation = _get_rotation_by_id(request, rotation_id)
consume = _get_rotation_consumption(rotation, _get_selected_farm(request))
free_paddocks = _get_free_paddocks(request)
return {
'rotation': rotation,
'consume': consume,
'free_paddocks': free_paddocks
}
@@ -0,0 +1,96 @@
from django.shortcuts import redirect
from django.contrib.auth import authenticate, login, logout
from django.db.models import Count, Q
from ..forms import UserLoginForm
def user_profile(request): # TO DO
...
def get_user_login_context(request):
context = {}
form = UserLoginForm()
context['fail_login'] = False
context['form'] = form
return context
def user_login(request):
form = UserLoginForm(request.POST)
if not form.is_valid():
return False
username = form.cleaned_data['login']
password = form.cleaned_data['password']
user = authenticate(request, username=username, password=password)
if user is None:
return False
login(request, user)
return True
def user_logout(request):
logout(request)
return redirect('entities:user:user_login')
def user_signin(request): # TO DO
...
def farm_select(request):
if 'selected_farm_id' in request.session:
return {'redirect': 'entities:farm:farm_home'}
farms_query = request.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
)
)
context = {
'farms': farms_with_counts
}
return {'context': context}
def farm_set(request, farm_id=None):
# Allows setting the selected farm either via URL parameter or POST data
farm_id = farm_id or request.POST.get("farm_id")
next_url = request.POST.get("next")
print(farm_id)
if farm_id == '0':
return farm_deselect(request)
if not farm_id:
return redirect('entities:user:farm_select')
try:
farm = request.user.farms.get(id=farm_id)
request.session['selected_farm_id'] = farm.id
return redirect(next_url or 'report:dashboard')
except request.user.farms.model.DoesNotExist:
return redirect('entities:user:farm_select')
def farm_deselect(request):
if 'selected_farm_id' in request.session:
del request.session['selected_farm_id']
return redirect('entities:user:farm_select')
@@ -0,0 +1,152 @@
/* ===== PAGE HEADER ===== */
.animal_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.animal_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_animal_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_animal_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
/* Input com altura igual ao botão */
.form_animal_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_animal_search input:focus {
border-color: var(--color-primary);
}
/* Botão buscar */
.form_animal_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_animal_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.animal_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.animal_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
/* ===== TABLE ===== */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-white);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
table th {
text-align: left;
padding: var(--space-sm);
font-size: var(--font-sm);
background: var(--color-bg-dark);
color: var(--color-text-light);
font-weight: 500;
}
table td {
padding: var(--space-sm);
font-size: var(--font-md);
border-bottom: 1px solid var(--color-border-subtle);
}
table tr:last-child td {
border-bottom: none;
}
table tbody tr:hover {
background: var(--color-bg-light);
}
/* ===== ACTION ICONS ===== */
table a {
color: var(--color-primary);
transition: color var(--transition-speed);
}
table a:hover {
color: var(--color-secondary);
}
/* DELETE BUTTON */
.btn-icon-trash {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
transition: color var(--transition-speed);
}
.btn-icon-trash:hover {
color: var(--color-danger);
opacity: 0.7;
}
@@ -0,0 +1,150 @@
/* ===== PAGE HEADER ===== */
.batch_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.batch_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_batch_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_batch_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_batch_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_batch_search input:focus {
border-color: var(--color-primary);
}
.form_batch_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_batch_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.batch_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.batch_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
/* ===== TABLE ===== */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-white);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
table th {
text-align: left;
padding: var(--space-sm);
font-size: var(--font-sm);
background: var(--color-bg-dark);
color: var(--color-text-light);
font-weight: 500;
}
table td {
padding: var(--space-sm);
font-size: var(--font-md);
border-bottom: 1px solid var(--color-border-subtle);
}
table tr:last-child td {
border-bottom: none;
}
table tbody tr:hover {
background: var(--color-bg-light);
}
/* ===== ACTION ICONS ===== */
table a {
color: var(--color-primary);
transition: color var(--transition-speed);
}
table a:hover {
color: var(--color-secondary);
}
/* DELETE BUTTON */
.btn-icon-trash {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
transition: color var(--transition-speed);
}
.btn-icon-trash:hover {
color: var(--color-danger);
opacity: 0.7;
}
@@ -0,0 +1,254 @@
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 30px;
}
.header-left p {
color: #6c757d;
}
/* Containers */
.farm-config-container,
.batch-type-config-container,
.allowed-users-container,
.available-farms-container {
background: white;
overflow: hidden;
padding: 22px;
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,.05);
}
/* Headers */
.allowed-users-header,
.batch-type-config-header,
.section-header,
.available-farms-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.allowed-users-title,
.available-farms-title,
.batch-type-title,
.farm-config-title {
margin: 0;
}
.allowed-users-header a,
.batch-type-config-header a,
.available-farms-header a {
display: flex;
align-items: center;
gap: 6px;
text-decoration: none;
font-size: 14px;
font-weight: 600;
color: white;
background-color: #198754;
border-radius: 5px;
padding: 3px 6px;
width: 120px;
justify-content: center;
}
/* Consumptions */
.farm-config-container form {
display: flex;
gap: 12px;
}
.config-card {
flex: 1;
background: #f8f9fa;
border-radius: 10px;
padding: 14px;
}
.config-card-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 3px;
margin-bottom: 5px;
font-size: 14px;
color: #6c757d;
}
.metric-question {
cursor: pointer;
}
.config-card input {
width: 100%;
padding: 10px;
border: 1px solid #dee2e6;
border-radius: 8px;
color: #44494e;
}
.config-card input:focus {
color: #000000;
outline-color: #6c757d;
}
.config-submit {
flex: .5;
display: flex;
}
.config-submit button {
width: 100%;
height: 50%;
margin: auto;
border: none;
border-radius: 10px;
background: #198754;
color: white;
font-weight: 600;
cursor: pointer;
transition: .15s ease;
}
.config-submit button:hover {
opacity: .92;
}
/* Batchtypes */
.batch-type-card {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 14px;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 10px;
}
.type-title {
font-weight: 600;
}
.type-consumption-factor {
color: #6c757d;
font-size: .9rem;
}
.batch-type-right {
display: flex;
gap: 14px;
}
.edit-type,
.delete-type {
text-decoration: none;
font-size: .9rem;
}
.edit-type {
color: #0d6efd;
}
.delete-type {
color: #dc3545;
}
/* Allowed Users */
.allowed-user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 10px;
}
.user-remove {
color: #dc3545;
text-decoration: none;
}
/* Farms */
.available-farms-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 14px;
}
.available-farms-header {
grid-column: 1 / -1;
}
.available-farm-card {
background: #f8f9fa;
border-radius: 10px;
padding: 16px;
}
.available-farm-name {
font-weight: 600;
font-size: 1rem;
margin-bottom: 10px;
}
.available-farm-ifo {
display: flex;
gap: 16px;
color: #6c757d;
margin-bottom: 14px;
}
.available-farm-options {
display: flex;
justify-content: space-between;
}
.available-farm-select {
text-decoration: none;
color: #198754;
font-weight: 600;
}
.available-farm-delete {
text-decoration: none;
cursor: pointer;
color: #dc3545;
}
@media (max-width: 900px) {
.header {
flex-direction: column;
align-items: stretch;
}
.header-right {
width: 100%;
flex-wrap: wrap;
}
.farm-info {
flex: 1;
}
.farm-config-container form {
flex-direction: column;
}
.config-submit button {
min-height: 48px;
width: 100%;
}
.allowed-users-header, .batch-type-config-header {
flex-wrap: wrap;
gap: 10px;
}
}
@@ -0,0 +1,94 @@
/* ===== PAGE HEADER ===== */
.paddock_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.paddock_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_paddock_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_paddock_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_paddock_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_paddock_search input:focus {
border-color: var(--color-primary);
}
.form_paddock_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_paddock_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.paddock_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.paddock_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
@@ -0,0 +1,75 @@
/* ===== PAGE HEADER ===== */
.paddock_group_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.paddock_group_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_paddock_group_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_paddock_group_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_paddock_group_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_paddock_group_search input:focus {
border-color: var(--color-primary);
}
.form_paddock_group_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_paddock_group_search button:hover {
background: var(--color-secondary);
}
/* ===== NEW BUTTON ===== */
.paddock_group_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.paddock_group_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
@@ -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;
}
@@ -0,0 +1,214 @@
/* PAGE HEADER */
.entity_search {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
margin-top: var(--space-lg);
}
/* SEARCH FORM */
.entity_search form {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.entity_search form label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
/* Input com altura igual ao botão */
.entity_search form input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.entity_search form input:focus {
border-color: var(--color-primary);
}
/* Botão buscar */
.entity_search form button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-neutral-btn);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.entity_search form button:hover {
background: var(--color-neutral-btn-hover);
}
/* CLEAR FILTER */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-bg-filter);
color: var(--color-text-filter);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger-btn-hover);
}
/* NEW BUTTON */
.new-entity-button {
cursor: pointer;
user-select: none;
height: 32px;
padding: 0 var(--space-md);
background: var(--color-plus-btn);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.new-entity-button:hover {
background: var(--color-plus-btn-hover);
opacity: 0.85;
}
/* TABLE */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-white);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
table th {
text-align: left;
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-md);
background: var(--color-bg-dark);
color: var(--color-text-light);
font-weight: 500;
}
table td {
padding: var(--space-xs);
font-size: var(--font-md);
border-bottom: 1px solid var(--color-border-subtle);
}
table tr:last-child td {
border-bottom: none;
}
table tbody tr:hover {
background: var(--color-bg-light);
}
/* Fixed width on the last two colluns */
td:last-child,
th:last-child {
width: 60px;
text-align: center;
}
td:nth-last-child(2),
th:nth-last-child(2) {
width: 60px;
text-align: center;
}
td, th {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ACTION ICONS */
table a {
color: var(--color-primary);
transition: color var(--transition-speed);
}
table a:hover {
color: var(--color-secondary);
}
/* DELETE BUTTON */
.btn-icon-trash {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
transition: color var(--transition-speed);
}
.btn-icon-trash:hover {
color: var(--color-danger);
opacity: 0.7;
}
/* PAGINATION */
.pagination {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-top: var(--space-lg);
font-size: var(--font-sm);
}
.pagination .left {
justify-self: end;
display: flex;
gap: var(--space-xs);
}
.pagination .center {
justify-self: center;
white-space: nowrap;
margin: 0 10px;
}
.pagination .right {
justify-self: start;
display: flex;
gap: var(--space-xs);
}
.pagination a {
padding: 3px 7px;
background: var(--color-bg-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
color: var(--color-text-dark);
transition: all var(--transition-speed);
}
@media (max-width: 768px) {
.entity_search {
flex-direction: column;
align-items: stretch;
}
.entity_search form {
margin-left: 0;
}
}
@@ -0,0 +1,80 @@
.open-rotations-container {
display: flex;
flex-direction: row;
gap: 15px;
overflow-x: auto;
scrollbar-width: none;
padding: 15px;
}
/* Active rotation cards */
.active-rotation-card {
width: 300px;
background: #ffffff;
color: #212529;
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
gap: 4px;
transition: 0.2s ease;
}
.active-rotation-card {
user-select: none;
}
.active-rotation-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
border-color: var(--color-primary);
cursor: pointer;
}
.active-rotation-card p {
margin: 0;
font-size: 14px;
}
.active-rotation-card .identification p {
font-weight: 600;
font-size: 16px;
letter-spacing: 0.5px;
}
.active-rotation-card .duration p {
font-size: 13px;
color: #6c757d;
}
.active-rotation-card .details {
margin-top: auto;
display: flex;
justify-content: flex-end;
}
.active-rotation-card .details p {
font-size: 12px;
color: #6c757d;
display: flex;
align-items: center;
gap: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@media (max-width: 760px){
.open-rotations-container {
flex-direction: column;
overflow-y: auto;
max-height: 300px;
align-items: center;
}
.active-rotation-card {
width: 100%;
}
}
@@ -0,0 +1,176 @@
.rotation-view-more {
display: flex;
flex-direction: column;
gap: 28px;
}
.rotation-view-more-rotation-status {
text-align: center;
}
.rotation-view-more-rotation-status h2 {
margin-top: 8px;
color: var(--color-primary);
}
.rotation-view-more-status-badge {
display: inline-block;
padding: 6px 13px;
border-radius: 999px;
background: rgba(46, 204, 113, .15);
color: #27ae60;
font-size: 13px;
font-weight: 600;
}
.rotation-view-more-rotation-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.rotation-view-more-card {
padding: 4px;
border-radius: 12px;
border: 1px solid var(--color-secondary);
text-align: center;
background: white;
}
.rotation-view-more-label {
display: block;
color: #777;
font-size: 13px;
margin-bottom: 6px;
}
.rotation-view-more-card strong {
font-size: 22px;
color: var(--color-primary);
}
.rotation-view-more-rotation-section h3,
.rotation-view-more-rotation-next h3 {
margin-bottom: 16px;
color: var(--color-primary);
}
.rotation-view-more-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.rotation-view-more-info {
display: flex;
flex-direction: column;
padding: 14px;
border: 1px solid #e5e5e5;
border-radius: 10px;
background: #fafafa;
}
.rotation-view-more-info span {
font-size: 13px;
color: #888;
margin-bottom: 5px;
}
.rotation-view-more-info strong {
color: var(--color-primary);
}
.rotation-view-more-rotation-next {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.rotation-view-more-rotation-next h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.rotation-view-more-rotation-next .rotation-next-actions {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 12px;
align-items: center;
}
.rotation-view-more-rotation-next select {
height: 44px;
width: 100%;
padding: 0 16px;
border-radius: 8px;
border: 1px solid #ddd;
background: white;
font-size: 15px;
color: #444;
outline: none;
cursor: pointer;
}
.rotation-view-more-rotation-next select:focus {
border-color: var(--color-primary);
}
.rotation-view-more-rotate,
.rotation-view-finish-rotate {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: .2s;
white-space: nowrap;
}
.rotation-view-more-rotate {
border: none;
background: var(--color-primary);
color: white;
}
.rotation-view-more-rotate:hover {
filter: brightness(.95);
}
.rotation-view-finish-rotate {
border: 1px solid var(--color-secondary);
background: white;
color: var(--color-secondary);
}
.rotation-view-finish-rotate:hover {
background: var(--color-secondary);
color: white;
}
@media (max-width: 700px) {
.rotation-view-more-rotation-cards {
grid-template-columns: 1fr;
}
.rotation-view-more-info-grid {
grid-template-columns: 1fr;
}
.rotation-view-more-rotation-next .rotation-next-actions {
grid-template-columns: 1fr;
}
.rotation-view-more-rotation-next select,
.rotation-view-more-rotate,
.rotation-view-finish-rotate {
width: 100%;
}
}
@@ -0,0 +1,52 @@
document.addEventListener("DOMContentLoaded", () => {
const container = document.querySelector(".open-rotations-container");
if (!container) return;
let targetScroll = container.scrollLeft;
let currentScroll = container.scrollLeft;
let isAnimating = false;
container.addEventListener("wheel", (event) => {
let isMobile = window.innerWidth <= 768;
if (isMobile) return;
event.preventDefault();
const maxScroll =
container.scrollWidth - container.clientWidth;
targetScroll += event.deltaY;
// Prevent ghost scrolling
targetScroll = Math.max(0, Math.min(targetScroll, maxScroll));
if (!isAnimating) {
isAnimating = true;
animateScroll();
}
});
function animateScroll() {
currentScroll += (targetScroll - currentScroll) * 0.15;
container.scrollLeft = currentScroll;
if (Math.abs(targetScroll - currentScroll) > 0.5) {
requestAnimationFrame(animateScroll);
} else {
currentScroll = targetScroll;
isAnimating = false;
}
}
});
document.addEventListener("DOMContentLoaded", function () {
const params = new URLSearchParams(window.location.search);
if (params.has("page")) {
document.querySelector("#table").scrollIntoView({
behavior: "auto"
});
}
});
@@ -0,0 +1,162 @@
/* CONTENT */
body {
display: block;
min-height: 100vh;
margin: 0;
padding-top: var(--header-height);
}
/* NAVBAR */
.farm-select-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
background-color: var(--color-bg-light);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--space-lg);
z-index: 20;
}
/* SITE'S TITLE */
.farm-select-navbar .navbar-brand {
font-weight: bold;
font-size: 1.5rem;
color: var(--color-bg-sidebar, #444);
cursor: default;
}
/* .navbar-user-info and .username uses gap and align style from global.css */
/* SELECT CONTAINER */
.farm-select-container {
width: 100%;
max-width: 700px;
margin: var(--space-xl) auto;
text-align: center;
padding: 0;
}
.farm-select-container h1 {
font-size: var(--font-xl);
color: var(--login-background-primary);
margin-bottom: var(--space-xl);
}
/* LIST OF FARMS */
.farm-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.farm-button {
text-decoration: none;
display: block;
width: 100%;
}
.farm-item {
padding: var(--space-md);
text-align: left;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-left: 5px solid transparent;
border-radius: 6px;
transition: all 0.3s ease;
}
.farm-item:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
border-color: var(--color-farm-select-hover);
border-left-color: var(--color-farm-select-hover); /* Cor primária */
}
.farm-name {
font-size: 1.25rem;
color: var(--color-text-dark);
margin: 0 0 var(--space-xs);
font-weight: 700;
}
.farm-description {
display: flex;
justify-content: flex-start;
gap: var(--space-sm);
align-items: center;
margin-top: var(--space-sm);
font-size: var(--font-sm);
flex-wrap: wrap;
}
/* DATA SEPARATOR */
.total-area, .count-piquetes, .count-animais {
padding: 0;
background-color: transparent;
color: var(--color-farm-select-info);
font-weight: 500;
}
.total-area:after, .count-piquetes:after {
content: "|";
margin-left: var(--space-xs);
color: var(--color-simple-light-gray);
}
/* WHEN NO FARMS */
.no-farms {
padding: var(--space-lg);
margin-top: var(--space-lg);
border: 2px dashed #ccc;
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-white);
}
.no-farms p {
color: var(--color-farm-select-info);
font-size: var(--font-md);
margin: var(--space-xs) 0;
}
/* NEW FARM BUTTON */
.new-farm-btn {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-top: var(--space-md);
padding: var(--space-xs) var(--space-md);
background-color: var(--color-new-farm-button);
color: white;
text-decoration: none;
font-weight: 500;
border-radius: 4px;
transition: background-color 0.2s;
}
.new-farm-btn:hover {
background-color: var(--color-new-farm-hover);
}
@media (max-width: 768px) {
body {
padding: 0;
}
.farm-select-navbar {
padding: 0 var(--space-md);
}
.farm-select-container {
padding: var(--space-md) var(--space-sm);
margin: var(--header-height) 0 0 0;
max-width: 100%;
box-shadow: none;
border-radius: 0;
}
}
@@ -0,0 +1,145 @@
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: var(--color-bg-light);
color: var(--color-text-dark);
}
.login-container {
display: flex;
width: 850px;
max-width: 90%;
min-height: 450px;
border-radius: var(--border-radius-lg, 10px);
overflow: hidden;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
background-color: #ffffff;
}
.ilustration-container {
flex: 1;
background: linear-gradient(135deg, var(--login-background-primary), var(--login-background-secondary));
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--space-lg);
color: var(--color-text-light);
text-align: center;
font-size: var(--font-lg, 1.5rem);
font-weight: bold;
}
.ilustration-container h1 {
margin: 0;
line-height: 1.2;
}
.login-form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--space-xl);
}
.login-form-container h1 {
font-size: var(--font-xl);
margin-bottom: var(--space-md);
color: var(--color-text-dark);
text-align: center;
}
.login-form-container form {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.login-form-container input[type="text"],
.login-form-container input[type="password"] {
padding: var(--space-sm);
font-size: var(--font-md);
border-radius: var(--border-radius-sm);
border: 1px solid var(--color-border-subtle);
width: 100%;
transition: border-color var(--transition-speed) ease;
}
.login-form-container input:focus {
border-color: var(--color-primary);
outline: none;
box-shadow: 0 0 0 1px var(--color-primary-light, rgba(0, 123, 255, 0.2));
}
.login-form-container button[type="submit"] {
background-color: var(--color-login-button);
color: white;
padding: var(--space-xs);
font-size: var(--font-lg);
font-weight: bold;
border: none;
border-radius: var(--border-radius-sm);
cursor: pointer;
margin-top: var(--space-sm);
transition: background-color var(--transition-speed) ease;
}
.login-form-container button[type="submit"]:hover {
background-color: var(--color-login-button-focus);
}
.login-fail {
color: var(--color-danger);
background-color: var(--color-danger-light);
border: 1px solid var(--color-danger);
padding: var(--space-xs);
border-radius: var(--border-radius-sm);
font-size: var(--font-sm);
text-align: center;
margin-top: var(--space-sm);
}
.login-fail i {
margin-right: var(--space-xs);
}
.login-form-container p {
text-align: center;
font-size: var(--font-sm);
margin-top: var(--space-md);
}
.login-form-container a {
color: var(--color-primary);
text-decoration: none;
}
.login-form-container a:hover {
text-decoration: underline;
}
@media (max-width: 800px) {
.login-container {
flex-direction: column;
height: auto;
max-width: 95%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.ilustration-container, .login-form-container {
flex: none;
width: 100%;
}
.ilustration-container {
padding: var(--space-md);
}
.ilustration-container h1 {
font-size: var(--font-md);
}
.login-form-container {
padding: var(--space-lg) var(--space-md);
}
}
@@ -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 %}
NOVO ANIMAL <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 ANIMAL <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/batch.css' %}">
{% endblock %}
{% block content %}
NOVO 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/batch.css' %}">
{% endblock %}
{% block content %}
EDITAR 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/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 %}
@@ -0,0 +1,12 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Nova Fazenda <br>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,105 @@
{% extends 'layouts/base.html' %}
{% load static %}
{% load l10n %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'farm/css/farm_home.css' %}">
{% endblock %}
{% block content %}
<div class="header">
<div class="header-left">
<h1 class="title">{{ selected_farm.name }}</h1>
<p>Configurações da fazenda</p>
</div>
</div>
<div class="farm-config-container">
<h2 class="farm-config-title">Métricas da fazenda</h2>
<form action="{% url "entities:farm:metrics_update" %}" method="POST">
{% csrf_token %}
<div class="config-card" title="Percentual do peso do animal correspondente ao seu consumo diário">
<p class="config-card-title">Consumo padrão <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="default-consume" min="0.01" step="0.01" value="{{ selected_farm.default_consumption|unlocalize }}" required>
</div>
<div class="config-card" title="Variação do consumo no verão (1,00 = sem variação)">
<p class="config-card-title">Consumo no verão <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="summer-consume" min="0.01" step="0.01" value="{{ selected_farm.summer_consumption|unlocalize }}" required>
</div>
<div class="config-card" title="Variação do consumo no inverno (1,00 = sem variação)">
<p class="config-card-title">Consumo no inverno <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="winter-consume" min="0.01" step="0.01" value="{{ selected_farm.winter_consumption|unlocalize }}" required>
</div>
<div class="config-card" title="Tempo ideal de ocupação em um piquete em dias">
<p class="config-card-title">Tempo de ocupação <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="ideal_paddock_occupation" min="1" step="1" value="{{ selected_farm.ideal_paddock_occupation|unlocalize }}" required>
</div>
<div class="config-card" title="Tempo ideal de descanço para uma piquete em dias">
<p class="config-card-title">Tempo de repouso <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="ideal_paddock_rest" min="1" step="1" value="{{ selected_farm.ideal_paddock_rest|unlocalize }}" required>
</div>
<div class="config-submit">
<button type="submit">Salvar</button>
</div>
</form>
</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 de Lote</a>
</div>
{% if batch_types|length < 1 %}
<p class="type-consumption-factor">Nenhum tipo de lote foi cadastrado ainda</p>
{% endif %}
{% 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 %}
</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">
<div class="available-farms-header">
<h2 class="available-farms-title">Suas fazendas</h2>
<a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Fazenda</a>
</div>
{% 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,10 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Update - Farm
{% 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 %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'layouts/entity_create.css' %}">
{% endblock %}
{% block content %}
<h1 class="entity-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,99 @@
{% extends 'layouts/base.html' %}
{% load static %}
{% load custom_tags %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'layouts/entity_list.css' %}">
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
{% block before_table %}{% endblock %}
<div class="entity_search">
<p>Busca geral</p>
<!-- Search -->
<form method="GET" action="{% url list_url %}">
{% if query %}
<a href="{% url list_url %}" class="btn-clear-filter">
{{ query }} <i class="fa-solid fa-xmark"></i>
</a>
{% endif %}
<label for="search">Buscar</label>
<input type="text" name="q" value="{{ query }}" required>
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
<!-- New -->
<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 %}
<p>Nenhum registro encontrado.</p>
{% else %}
{% block table %}
<table>
<tr>
{% for col in columns %}
<th>{{ col.label }}</th>
{% endfor %}
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for obj in object_list %}
<tr>
{% for col in columns %}
<td>
{{ obj|get_attr:col.field }}
</td>
{% endfor %}
<!-- Update -->
<td>
<a class="entity-update" hx-get="{% url edit_url obj.id %}" hx-swap="beforeend" hx-target="body">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</td>
<!-- Delete -->
<td>
<a class="entity-delete" hx-get="{% url delete_url obj.id %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
<!-- Pagination -->
{% if is_paginated %}
<div class="pagination">
<div class="left">
{% if page_obj.has_previous %}
<a href="?page=1&q={{ query }}"><i class="fa-solid fa-angles-left"></i></a>
<a href="?page={{ page_obj.previous_page_number }}&q={{ query }}"><i class="fa-solid fa-angle-left"></i></a>
{% endif %}
</div>
<span class="center">
Página {{ page_obj.number }} de {{ page_obj.paginator.num_pages }}
</span>
<div class="right">
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&q={{ query }}"><i class="fa-solid fa-angle-right"></i></a>
<a href="?page={{ page_obj.paginator.num_pages }}&q={{ query }}"><i class="fa-solid fa-angles-right"></i></a>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% block after_table %}{% endblock %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'layouts/entity_update.css' %}">
{% endblock %}
{% block content %}
<h1 class="entity-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Paddock
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Edit - Paddock
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Paddock Group
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Delete - Paddock Group
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1 @@
MOVE TEMPLATE
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Rotation
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,75 @@
{% extends 'layouts/entity_list.html' %}
{% load custom_tags %}
{% load static %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'rotation/css/rotation_list.css' %}">
{% endblock extra_css %}
{% block before_table %}
{% if open_rotations %}
<p>Movimentações em aberto</p>
<div class="open-rotations-container">
{% for obj in open_rotations %}
<div class="active-rotation-card" hx-get="{% url 'entities:rotation:view_more' obj.id %}" hx-swap="beforeend" hx-target="body">
<div class="identification">
<p><i class="fa-solid fa-cow"></i> Lote {{ obj.batch.identifier }} em piquete {{ obj.paddock.paddock_number }}</p>
</div>
<div class="duration">
<p><i class="fa-solid fa-arrow-rotate-right"></i> Há {{ obj.duration_display_day }} dias, desde {{ obj.entry_date|date:"d/m/Y" }}</p>
</div>
<div class="animals">
<p>Animais: {{ obj.animal_count }}</p>
</div>
<div class="livestock">
<p>UGM: {{ obj.livestock_unit }}</p>
</div>
<div class="details">
<p>Ver mais <i class="fa-solid fa-angle-right"></i></p>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block table %}
<table id="table">
<tr>
{% for col in columns %}
<th>{{ col.label }}</th>
{% endfor %}
{% comment %} <th>Excluir</th> {% endcomment %}
</tr>
{% for obj in object_list %}
<tr>
{% for col in columns %}
<td>
{{ obj|get_attr:col.field }}
</td>
{% endfor %}
<!-- Delete -->
{% comment %} <td>
<form method="POST" action="{% url delete_url obj.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td> {% endcomment %}
</tr>
{% endfor %}
</table>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script src="{% static "rotation/js/rotation_list.js" %}"></script>
{% endblock extra_js %}
@@ -0,0 +1,101 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<div class="rotation-view-more">
<div class="rotation-view-more-rotation-status">
<span class="rotation-view-more-status-badge">● Em andamento</span>
<h2>Movimentação #{{ rotation.id }}</h2>
</div>
<div class="rotation-view-more-rotation-cards">
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">Tempo no piquete</span>
<strong>{{ rotation.duration_display_day }} dias</strong>
</div>
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">UGM-dia</span>
<strong>{{ rotation.livestock_unit_day }}</strong>
</div>
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">Animais-dia</span>
<strong>{{ rotation.animal_count_day }}</strong>
</div>
</div>
<div class="rotation-view-more-rotation-section">
<h3>Informações</h3>
<div class="rotation-view-more-info-grid">
<div class="rotation-view-more-info">
<span>Lote</span>
<strong>{{ rotation.batch|default:"—" }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Piquete</span>
<strong>{{ rotation.paddock }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Entrada</span>
<strong>{{ rotation.entry_date|date:"d/m/Y H:i" }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Animais</span>
<strong>{{ rotation.animal_count }}</strong>
</div>
<div class="rotation-view-more-info">
<span>UGM</span>
<strong>{{ rotation.livestock_unit }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Consumo</span>
<strong>{{ consume }} Kg</strong>
</div>
</div>
</div>
<div class="rotation-view-more-rotation-next">
<h3>
<i class="fa-solid fa-rotate"></i>
Rotacionar lote
</h3>
<div class="rotation-next-actions">
{% if free_paddocks|length > 0 %}
<select name="next_paddock">
<option selected disabled>Selecione o próximo piquete</option>
{% for paddock in free_paddocks %}
<option value="{{ paddock.id }}">{{ paddock.paddock_number }} | {% if paddock.current_rest %}Descanço: {{ paddock.current_rest }} dias {% else %}nunca utilizado{% endif %}</option>
{% endfor %}
</select>
<button class="rotation-view-more-rotate">
<i class="fa-solid fa-rotate"></i>
Dar andamento
</button>
{% endif %}
<button class="rotation-view-finish-rotate">
<i class="fa-solid fa-clipboard-check"></i>
Encerrar movimentação
</button>
</div>
</div>
</div>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -0,0 +1,10 @@
from django.urls import include, path
app_name = "entities"
urlpatterns = [
path("animal/", include("entities.urls.animal", namespace='animal')),
path("farm/", include("entities.urls.farm", namespace='farm')),
path("rotation/", include("entities.urls.rotation", namespace='rotation')),
path("user/", include("entities.urls.user", namespace='user')),
]
+22
View File
@@ -0,0 +1,22 @@
from django.urls import path
from ..views import animal as views
from ..views import AnimalListView, BatchListView
app_name = 'animal'
urlpatterns = [
path('list/', AnimalListView.as_view(), name='animal_list'),
path('batch/list/', BatchListView.as_view(), name='batch_list'),
path('create/', views.animal_create, name='animal_create'),
path('update/<int:animal_id>', views.animal_update, name='animal_update'),
path('delete/<int:animal_id>', views.animal_delete, name='animal_delete'),
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'),
]
+29
View File
@@ -0,0 +1,29 @@
from django.urls import path
from ..views import farm as views
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'),
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('metrics/update', views.metrics_update, name='metrics_update'),
path('paddock/create/', views.paddock_create, name='paddock_create'),
path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'),
path('paddock/delete/<int:paddock_id>', views.paddock_delete, name='paddock_delete'),
path('paddock_group/create/', views.paddock_group_create, name='paddock_group_create'),
path('paddock_group/update/<int:paddock_group_id>', views.paddock_group_update, name='paddock_group_update'),
path('paddock_group/delete/<int:paddock_group_id>', views.paddock_group_delete, name='paddock_group_delete'),
]
@@ -0,0 +1,13 @@
from django.urls import path
from ..views import RotationListView
from ..views import rotation as views
app_name = 'rotation'
urlpatterns = [
path('', RotationListView.as_view(), name='rotation_home'),
path('create/', views.rotation_create, name='rotation_create'),
# path('delete/<int:rotation_id>', views.rotation_delete, name='rotation_delete'),
path('more/<int:rotation_id>', views.rotation_view_more, name='view_more'),
]
+18
View File
@@ -0,0 +1,18 @@
from django.urls import path
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from ..views import user as views
app_name = 'user'
urlpatterns = [
path('', views.user_profile, name='user_profile'),
path('login/', views.user_login, name='user_login'),
path('logout/', views.user_logout, name='user_logout'),
path('signin/', views.user_signin, name='user_signin'),
path('farm-select/', views.farm_select, name='farm_select'),
path('farm-set/<int:farm_id>/', views.farm_set, name='farm_set'),
path('farm-set/', views.farm_set, name='farm_set_post'),
path('farm-deselect/', views.farm_deselect, name='farm_deselect'),
]
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more