Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/pr-1-rooms-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PR 1 - Filtro de Habitaciones

## Objetivo de negocio
Incorporar un mecanismo de búsqueda en la sección **Habitaciones** para mejorar la localización de cuartos por nombre y reducir tiempo operativo en front desk/soporte.

## Alcance implementado
- Se agregó un formulario de filtro por nombre en la vista de Habitaciones.
- El filtro opera por coincidencia parcial (`icontains`) sobre `Room.name`.
- Se mantiene el comportamiento original cuando no hay criterio de búsqueda (listado completo).
- Se agregó estado vacío cuando no existen coincidencias.
- Se mejoró la presentación de los controles de búsqueda para una experiencia más clara y consistente.

## Cambios funcionales y técnicos
1. **Backend**
- Se actualizó la lógica de `RoomsView` para aceptar parámetro `name` y filtrar resultados.
- Se agregó un formulario dedicado de filtro para desacoplar validación y render.

2. **Frontend**
- Se incorporó barra de filtro con input, botón de búsqueda y acción de limpieza.
- Se añadió feedback contextual del total de resultados.
- Se mejoró el layout visual del bloque de controles para reforzar jerarquía y legibilidad.

3. **Calidad**
- Se incorporaron pruebas automatizadas que validan:
- listado completo sin filtro,
- filtro por coincidencia parcial,
- estado sin resultados.

## Archivos modificados
- `pms/forms.py`
- `pms/views.py`
- `pms/templates/rooms.html`
- `pms/statics/css/style.css`
- `pms/tests.py`

## Pruebas ejecutadas
- `python manage.py test pms.tests.RoomsFilterTests`

## Resultado esperado para el producto
- Menor fricción en operación diaria al buscar habitaciones.
- Mejor percepción de calidad UI en una pantalla de uso frecuente.
- Base funcional y visual estable para iteraciones futuras en catálogo/gestión de habitaciones.
14 changes: 14 additions & 0 deletions pms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,17 @@ class Meta:
'total': forms.HiddenInput(),
'state': forms.HiddenInput(),
}


class RoomFilterForm(forms.Form):
name = forms.CharField(
required=False,
label="Buscar habitación",
widget=forms.TextInput(
attrs={
"placeholder": "Ej: Room 1",
"class": "form-control rooms-filter-input",
"autocomplete": "off",
}
),
)
170 changes: 169 additions & 1 deletion pms/statics/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,172 @@ body{
justify-content: center;
height: 100%;
align-items: center;
}
}

.rooms-page{
--rooms-surface: #ffffff;
--rooms-muted: #6c757d;
--rooms-line: #d9e1e7;
--rooms-brand: #0f766e;
--rooms-brand-soft: #e6f4f3;
}

.rooms-hero{
background: linear-gradient(135deg, #eff6ff 0%, #f0fdf4 100%);
border: 1px solid #d9e1e7;
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1rem;
}

.rooms-title{
margin: 0;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: -0.02em;
}

.rooms-subtitle{
margin: 0.4rem 0 0;
color: var(--rooms-muted);
}

.rooms-toolbar{
background: var(--rooms-surface);
border-radius: 16px;
padding: 1rem;
margin-bottom: 1.2rem;
}

.rooms-controls-panel{
border: 1px solid var(--rooms-line);
}

.rooms-filter-form{
display: flex;
align-items: flex-end;
gap: 0.6rem;
flex-wrap: wrap;
}

.rooms-filter-field{
flex: 1 1 360px;
min-width: 240px;
}

.rooms-filter-label{
display: inline-block;
margin-bottom: 0.35rem;
color: var(--rooms-muted);
font-size: 0.9rem;
font-weight: 600;
}

.rooms-filter-input-wrap{
position: relative;
}

.rooms-filter-icon{
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--rooms-muted);
}

.rooms-filter-input{
border-radius: 999px;
border: 1px solid var(--rooms-line);
padding-left: 2.2rem;
height: 44px;
}

.rooms-filter-input:focus{
border-color: var(--rooms-brand);
box-shadow: 0 0 0 0.2rem rgba(15, 118, 110, 0.15);
}

.rooms-filter-btn{
border-radius: 999px;
background: var(--rooms-brand);
border-color: var(--rooms-brand);
min-width: 100px;
height: 44px;
}

.rooms-filter-btn:hover{
background: #0a5e57;
border-color: #0a5e57;
}

.rooms-filter-clear{
border-radius: 999px;
height: 44px;
}

.rooms-results-meta{
margin-top: 0.75rem;
color: var(--rooms-muted);
font-size: 0.95rem;
}

.rooms-results-chip{
display: inline-flex;
border: 1px solid var(--rooms-line);
border-radius: 999px;
padding: 0.35rem 0.8rem;
background: rgba(15, 118, 110, 0.05);
}

.rooms-grid{
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

.rooms-card{
background: var(--rooms-surface);
border: 1px solid var(--rooms-line);
border-radius: 14px;
padding: 1rem;
transition: transform 180ms ease, box-shadow 180ms ease;
}

.rooms-card:hover{
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(16, 24, 40, 0.08);
}

.rooms-card-head{
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.8rem;
margin-bottom: 0.9rem;
}

.rooms-card-title{
margin: 0;
font-size: 1.05rem;
font-weight: 700;
}

.rooms-type-chip{
background: var(--rooms-brand-soft);
color: var(--rooms-brand);
border-radius: 999px;
font-size: 0.82rem;
font-weight: 600;
padding: 0.25rem 0.65rem;
white-space: nowrap;
}

.rooms-card-actions{
display: flex;
justify-content: flex-start;
}

.rooms-empty-state{
border-radius: 14px;
border: 1px solid #ffe58f;
}
60 changes: 47 additions & 13 deletions pms/templates/rooms.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,53 @@
{% extends "main.html"%}

{% block content %}
<h1>Habitaciones del hotel</h1>
{% for room in rooms%}
<div class="row card mt-3 mb-3 hover-card bg-tr-250">
<div class="col p-3">
<div class="">
{{room.name}} ({{room.room_type__name}})
</div>
<div>
<a href="{% url 'room_details' pk=room.id%}">Ver detalles</a>
<section class="rooms-page">
<div class="rooms-hero">
<h1 class="rooms-title">Habitaciones del hotel</h1>
<p class="rooms-subtitle">Filtra por nombre y encuentra la habitación en segundos.</p>
</div>

<div class="rooms-toolbar card border-0 shadow-sm rooms-controls-panel">
<form class="rooms-filter-form" method="get" action="{% url 'rooms' %}">
<div class="rooms-filter-field">
<label class="rooms-filter-label" for="{{ room_filter_form.name.id_for_label }}">Nombre de habitación</label>
<div class="rooms-filter-input-wrap">
<i class="bi bi-search rooms-filter-icon"></i>
{{ room_filter_form.name }}
</div>
</div>
<button class="btn btn-primary rooms-filter-btn" type="submit">Buscar</button>
<a class="btn btn-outline-secondary rooms-filter-clear" href="{% url 'rooms' %}">Limpiar</a>
</form>
<div class="rooms-results-meta rooms-results-chip">
{% if room_name_filter %}
<span>Resultados para <strong>"{{ room_name_filter }}"</strong>: {{ results_count }}</span>
{% else %}
<span>Total habitaciones: {{ results_count }}</span>
{% endif %}
</div>

</div>

</div>
{% endfor %}

{% if rooms %}
<div class="rooms-grid">
{% for room in rooms %}
<article class="rooms-card hover-card">
<div class="rooms-card-head">
<h2 class="rooms-card-title">{{ room.name }}</h2>
<span class="rooms-type-chip">{{ room.room_type__name }}</span>
</div>
<div class="rooms-card-actions">
<a class="btn btn-sm btn-outline-primary" href="{% url 'room_details' pk=room.id %}">
Ver detalles
</a>
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning rooms-empty-state" role="alert">
No se encontraron habitaciones para ese criterio.
</div>
{% endif %}
</section>
{% endblock content%}
35 changes: 34 additions & 1 deletion pms/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse

# Create your tests here.
from .models import Room, Room_type


@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
class RoomsFilterTests(TestCase):
@classmethod
def setUpTestData(cls):
room_type = Room_type.objects.create(name="Simple", price=20, max_guests=1)
Room.objects.create(room_type=room_type, name="Room 1.1", description="Desc")
Room.objects.create(room_type=room_type, name="Room 1.2", description="Desc")
Room.objects.create(room_type=room_type, name="Room 2.1", description="Desc")

def test_rooms_page_without_filter_displays_all_rooms(self):
response = self.client.get(reverse("rooms"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Room 1.1")
self.assertContains(response, "Room 1.2")
self.assertContains(response, "Room 2.1")
self.assertContains(response, "Total habitaciones: 3")

def test_rooms_page_filters_by_partial_name(self):
response = self.client.get(reverse("rooms"), {"name": "Room 1"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Resultados para <strong>"Room 1"</strong>: 2', html=True)
self.assertContains(response, "Room 1.1")
self.assertContains(response, "Room 1.2")
self.assertNotContains(response, "Room 2.1")

def test_rooms_page_shows_empty_state_when_no_matches(self):
response = self.client.get(reverse("rooms"), {"name": "Suite"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No se encontraron habitaciones para ese criterio.")
17 changes: 14 additions & 3 deletions pms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,20 @@ def get(self, request, pk):

class RoomsView(View):
def get(self, request):
# renders a list of rooms
rooms = Room.objects.all().values("name", "room_type__name", "id")
room_filter_form = RoomFilterForm(request.GET or None)
room_name = ""
rooms = Room.objects.all()
if room_filter_form.is_valid():
room_name = room_filter_form.cleaned_data.get("name", "").strip()
if room_name:
rooms = rooms.filter(name__icontains=room_name)

results_count = rooms.count()
rooms = rooms.values("name", "room_type__name", "id")
context = {
'rooms': rooms
'rooms': rooms,
'room_filter_form': room_filter_form,
'room_name_filter': room_name,
'results_count': results_count,
}
return render(request, "rooms.html", context)