Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dd945798a | ||
|
|
740547a8ed | ||
|
|
8d61dc60a8 | ||
|
|
39bd2be04a | ||
|
|
acd8225c3d | ||
|
|
7e722c4294 | ||
|
|
35620936ac | ||
|
|
edc0f2170f | ||
|
|
0a814d953b | ||
|
|
287cb258a4 | ||
|
|
cf91224602 | ||
|
|
ed31a1a3da | ||
|
|
e9b54535d3 | ||
|
|
d7887019f7 | ||
|
|
d3648237fb | ||
|
|
e64f8078d2 | ||
|
|
04fc99bd3e | ||
|
|
ae0483ddf1 | ||
|
|
9374bfd71b | ||
|
|
d3883669f3 | ||
|
|
24650484f1 |
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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 "- !"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "core"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("null/", views.null_response, name="null"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def null_response(request):
|
||||||
|
return HttpResponse('<div class="null"></div>')
|
||||||
Whitespace-only changes.
@@ -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)
|
||||||
@@ -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 *
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-05-30 19:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('entities', '0002_batchtype_alter_batch_batch_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='farm',
|
||||||
|
name='default_consumption',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='farm',
|
||||||
|
name='summer_consumption',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='farm',
|
||||||
|
name='winter_consumption',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 *
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -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 %}
|
||||||
@@ -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')),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
]
|
||||||
@@ -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
Reference in new issue
Block a user