feat: backoffice setup
CI / ci (ubuntu-24.04, 3.13) (push) Has been cancelled

This commit is contained in:
2026-07-23 13:31:53 +02:00
parent 87405f31fa
commit 2a9407e676
45 changed files with 2148 additions and 1 deletions
+68
View File
@@ -0,0 +1,68 @@
{% load static %}
<!DOCTYPE html>
<html lang="es" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}Backoffice{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css"/>
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
{% block extra_js %}
{% endblock %}
</head>
<body class="min-h-screen bg-base-200">
<div class="flex min-h-screen">
<aside class="w-64 shrink-0 bg-base-100 border-r border-base-300">
<a href="{% url 'backoffice:dashboard' %}" class="text-xl font-semibold block p-4">Backoffice</a>
<ul class="menu w-full">
<li>
<a href="{% url 'backoffice:product_list' %}" class="{% if section == 'products' %}menu-active{% endif %}">Productos</a>
</li>
<li>
<a href="{% url 'backoffice:order_list' %}" class="{% if section == 'orders' %}menu-active{% endif %}">Pedidos</a>
</li>
<li>
<a href="{% url 'backoffice:payment_list' %}" class="{% if section == 'payments' %}menu-active{% endif %}">Pagos</a>
</li>
<li>
<a href="{% url 'backoffice:customer_list' %}" class="{% if section == 'customers' %}menu-active{% endif %}">Clientes</a>
</li>
<li>
<a href="{% url 'backoffice:provider_list' %}" class="{% if section == 'providers' %}menu-active{% endif %}">Proveedores</a>
</li>
<li>
<a href="{% url 'backoffice:tax_list' %}" class="{% if section == 'taxes' %}menu-active{% endif %}">Impuestos</a>
</li>
<li>
<a href="{% url 'backoffice:brand_settings' %}" class="{% if section == 'settings' %}menu-active{% endif %}">Marca y ajustes</a>
</li>
</ul>
</aside>
<main class="flex-1 p-6">
{% block main %}
{% endblock %}
</main>
</div>
<dialog id="backoffice-modal" class="modal">
<div class="modal-box" id="backoffice-modal-box"></div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
document.body.addEventListener('htmx:afterSwap', function (event) {
if (event.detail.target && event.detail.target.id === 'backoffice-modal-box') {
document.getElementById('backoffice-modal').showModal();
}
});
document.body.addEventListener('backoffice:modal-close', function () {
document.getElementById('backoffice-modal').close();
});
</script>
</body>
</html>
@@ -0,0 +1,44 @@
{% extends 'backoffice/base.html' %}
{% block title %}{{ object.email }}{% endblock %}
{% block main %}
<h1 class="text-2xl font-semibold mb-1">{{ object.email }}</h1>
<p class="mb-6 opacity-70">{{ object.first_name }} {{ object.last_name }}</p>
<h2 class="text-xl font-semibold mb-2">Direcciones</h2>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300 mb-6">
<table class="table">
<thead><tr><th>Tipo</th><th>Dirección</th><th>Localidad</th></tr></thead>
<tbody>
{% for address in addresses %}
<tr class="hover">
<td><span class="badge badge-ghost">{{ address.get_address_type_display }}</span></td>
<td>{{ address.address }}</td>
<td>{{ address.address_town }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-6">Sin direcciones registradas.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<h2 class="text-xl font-semibold mb-2">Pedidos</h2>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
<table class="table">
<thead><tr><th>Código</th><th>Estado</th><th>Total</th></tr></thead>
<tbody>
{% for order in orders %}
<tr class="hover">
<td><a class="link" href="{% url 'backoffice:order_detail' order.pk %}">{{ order.code }}</a></td>
<td><span class="badge badge-ghost">{{ order.get_status_display }}</span></td>
<td>{{ order.total }} €</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-6">Sin pedidos.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
@@ -0,0 +1,17 @@
{% extends 'backoffice/base.html' %}
{% block title %}Backoffice{% endblock %}
{% block main %}
<h1 class="text-2xl font-semibold mb-6">Backoffice</h1>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
{% for section in sections %}
<a href="{% url section.url_name %}" class="card bg-base-100 border border-base-300 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body">
<h2 class="card-title">{{ section.label }}</h2>
</div>
</a>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,10 @@
<h3 class="text-lg font-bold mb-4">Confirmar borrado</h3>
<p class="mb-4">¿Seguro que quieres borrar «{{ object }}»?</p>
<form method="post" hx-post="{{ request.path }}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">
{% csrf_token %}
<div class="modal-action">
<button class="btn btn-error">Borrar</button>
<button type="button" class="btn" onclick="document.getElementById('backoffice-modal').close()">Cancelar</button>
</div>
</form>
@@ -0,0 +1,10 @@
<h3 class="text-lg font-bold mb-4">{{ title }}</h3>
<form method="post" enctype="multipart/form-data" hx-encoding="multipart/form-data" hx-post="{{ request.path }}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">
{% csrf_token %}
{{ form.as_p }}
<div class="modal-action">
<button class="btn btn-primary">Guardar</button>
<button type="button" class="btn" onclick="document.getElementById('backoffice-modal').close()">Cancelar</button>
</div>
</form>
@@ -0,0 +1,52 @@
{% load backoffice_extras %}
<div id="backoffice-list"
hx-trigger="backoffice:list-changed from:body"
hx-get="{{ request.get_full_path }}"
hx-target="#backoffice-list"
hx-swap="outerHTML">
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
<table class="table">
<thead>
<tr>
{% for label, attr in columns %}<th>{{ label }}</th>{% endfor %}
{% if update_url_name or delete_url_name or detail_url_name %}<th></th>{% endif %}
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr class="hover">
{% for label, attr in columns %}<td>{{ object|getattribute:attr }}</td>{% endfor %}
{% if update_url_name or delete_url_name or detail_url_name %}
<td class="text-right whitespace-nowrap">
{% if detail_url_name %}
<a class="btn btn-ghost btn-xs" href="{% url detail_url_name object.pk %}">Ver</a>
{% endif %}
{% if update_url_name %}
<button class="btn btn-ghost btn-xs" hx-get="{% url update_url_name object.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Editar</button>
{% endif %}
{% if delete_url_name %}
<button class="btn btn-ghost btn-xs text-error" hx-get="{% url delete_url_name object.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Borrar</button>
{% endif %}
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{{ columns|length|add:1 }}" class="text-center py-6">No hay resultados.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %}
<div class="join mt-4">
{% if page_obj.has_previous %}
<a class="join-item btn" href="?page={{ page_obj.previous_page_number }}">«</a>
{% endif %}
<span class="join-item btn btn-disabled">{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a class="join-item btn" href="?page={{ page_obj.next_page_number }}">»</a>
{% endif %}
</div>
{% endif %}
</div>
@@ -0,0 +1,16 @@
{% extends 'backoffice/base.html' %}
{% block title %}Confirmar borrado{% endblock %}
{% block main %}
<h1 class="text-2xl font-semibold mb-4">Confirmar borrado</h1>
<p class="mb-4">¿Seguro que quieres borrar «{{ object }}»?</p>
<form method="post" class="max-w-xl">
{% csrf_token %}
<div class="flex gap-2">
<button class="btn btn-error">Borrar</button>
{% if cancel_url %}<a class="btn" href="{{ cancel_url }}">Cancelar</a>{% endif %}
</div>
</form>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends 'backoffice/base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block main %}
<h1 class="text-2xl font-semibold mb-4">{{ title }}</h1>
<form method="post" enctype="multipart/form-data" class="max-w-xl">
{% csrf_token %}
{{ form.as_p }}
<div class="flex gap-2">
<button class="btn btn-primary">Guardar</button>
{% if cancel_url %}<a class="btn" href="{{ cancel_url }}">Cancelar</a>{% endif %}
</div>
</form>
{% endblock %}
@@ -0,0 +1,14 @@
{% extends 'backoffice/base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block main %}
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-semibold">{{ title }}</h1>
{% if create_url %}
<button class="btn btn-primary" hx-get="{{ create_url }}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir</button>
{% endif %}
</div>
{% include 'backoffice/generic/_list_fragment.html' %}
{% endblock %}
@@ -0,0 +1,74 @@
{% extends 'backoffice/base.html' %}
{% block title %}Pedido {{ object.code }}{% endblock %}
{% block main %}
<h1 class="text-2xl font-semibold mb-4">Pedido {{ object.code }}</h1>
<div class="stats shadow mb-6 bg-base-100">
<div class="stat">
<div class="stat-title">Cliente</div>
<div class="stat-value text-lg">{{ object.email }}</div>
</div>
<div class="stat">
<div class="stat-title">Estado</div>
<div class="stat-value text-lg"><span class="badge badge-lg">{{ object.get_status_display }}</span></div>
</div>
<div class="stat">
<div class="stat-title">Total</div>
<div class="stat-value text-lg">{{ object.total }} €</div>
</div>
</div>
<p class="mb-6">
<span class="font-medium">Dirección de envío:</span>
{{ object.shipping_address }}, {{ object.shipping_city }}, {{ object.shipping_zip }}
</p>
<h2 class="text-xl font-semibold mb-2">Estado de envío</h2>
<form method="post" action="{% url 'backoffice:order_update_shipping_status' object.pk %}" class="mb-6 max-w-sm">
{% csrf_token %}
{{ shipping_status_form.as_p }}
<button class="btn btn-primary btn-sm">Actualizar</button>
</form>
<h2 class="text-xl font-semibold mb-2">Líneas</h2>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300 mb-6">
<table class="table">
<thead><tr><th>Producto</th><th>Cantidad</th><th>Precio</th><th>Total</th></tr></thead>
<tbody>
{% for line in lines %}
<tr class="hover">
<td>
{{ line.product.name }}
{% if line.variant %}
{% for attribute_value in line.variant.attribute_values.all %}<span class="badge badge-ghost ml-1">{{ attribute_value }}</span>{% endfor %}
{% endif %}
</td>
<td>{{ line.quantity }}</td>
<td>{{ line.price }} €</td>
<td>{{ line.total }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h2 class="text-xl font-semibold mb-2">Pagos</h2>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
<table class="table">
<thead><tr><th>Fecha</th><th>Importe</th><th>Método</th></tr></thead>
<tbody>
{% for payment in payments %}
<tr class="hover">
<td>{{ payment.creation_date }}</td>
<td>{{ payment.amount }} €</td>
<td>{{ payment.get_method_display }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-6">Sin pagos registrados.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
@@ -0,0 +1,33 @@
<div id="backoffice-detail"
hx-trigger="backoffice:list-changed from:body"
hx-get="{{ request.path }}"
hx-target="#backoffice-detail"
hx-swap="outerHTML">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-semibold">{{ object.name }}</h1>
<button class="btn btn-sm" hx-get="{% url 'backoffice:product_attribute_update' object.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Editar nombre</button>
</div>
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold">Valores</h2>
<button class="btn btn-sm btn-primary" hx-get="{% url 'backoffice:product_attribute_value_create' object.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir valor</button>
</div>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
<table class="table">
<thead><tr><th>Valor</th><th></th></tr></thead>
<tbody>
{% for value in values %}
<tr class="hover">
<td>{{ value.value }}</td>
<td class="text-right">
<button class="btn btn-ghost btn-xs text-error" hx-get="{% url 'backoffice:product_attribute_value_delete' value.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Borrar</button>
</td>
</tr>
{% empty %}
<tr><td colspan="2" class="text-center py-6">Este atributo no tiene valores todavía.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,105 @@
{% load static %}
<div id="backoffice-detail"
hx-trigger="backoffice:list-changed from:body"
hx-get="{{ request.path }}"
hx-target="#backoffice-detail"
hx-swap="outerHTML">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-semibold">{{ product.name }}</h1>
<div class="flex gap-2">
<button class="btn btn-sm" hx-get="{% url 'backoffice:product_update' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Editar</button>
<button class="btn btn-sm btn-error" hx-get="{% url 'backoffice:product_delete' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Borrar</button>
</div>
</div>
<div class="stats shadow mb-6 bg-base-100">
<div class="stat">
<div class="stat-title">SKU</div>
<div class="stat-value text-lg">{{ product.sku }}</div>
</div>
<div class="stat">
<div class="stat-title">Stock</div>
<div class="stat-value text-lg">{{ product.stock }}</div>
</div>
<div class="stat">
<div class="stat-title">Precio actual</div>
<div class="stat-value text-lg">
{% if product.price %}{{ product.price.price_with_tax }} €{% else %}—{% endif %}
</div>
</div>
</div>
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold">Imágenes</h2>
<button class="btn btn-sm btn-primary" hx-get="{% url 'backoffice:product_image_create' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir imagen</button>
</div>
<div class="flex flex-wrap gap-4 mb-6">
{% for image in images %}
<div class="relative">
<img class="w-24 h-24 object-cover rounded-box border border-base-300" src="{{ image.s.url }}" alt="">
<button class="btn btn-xs btn-error absolute -top-2 -right-2" hx-get="{% url 'backoffice:product_image_delete' image.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML"></button>
</div>
{% empty %}
<p class="opacity-70">Este producto no tiene imágenes.</p>
{% endfor %}
</div>
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold">Variantes</h2>
<button class="btn btn-sm btn-primary" hx-get="{% url 'backoffice:product_variant_create' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir variante</button>
</div>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300 mb-6">
<table class="table">
<thead><tr><th>SKU</th><th>Atributos</th><th>Stock</th><th>Precio</th><th></th></tr></thead>
<tbody>
{% for variant in variants %}
<tr class="hover">
<td>{{ variant.sku }}</td>
<td>{% for attribute_value in variant.attribute_values.all %}<span class="badge badge-ghost mr-1">{{ attribute_value }}</span>{% endfor %}</td>
<td>{{ variant.stock }}</td>
<td>
{% if variant.price %}{{ variant.price.price_with_tax }} €{% else %}—{% endif %}
<button class="link text-sm ml-2" hx-get="{% url 'backoffice:product_variant_price_create' variant.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">+ precio</button>
</td>
<td class="text-right whitespace-nowrap">
<button class="btn btn-ghost btn-xs" hx-get="{% url 'backoffice:product_variant_update' variant.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Editar</button>
<button class="btn btn-ghost btn-xs text-error" hx-get="{% url 'backoffice:product_variant_delete' variant.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Borrar</button>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center py-6">Este producto no tiene variantes.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mb-6">
<button class="btn btn-sm" hx-get="{% url 'backoffice:product_price_create' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir precio al producto</button>
</div>
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold">Remesas</h2>
<button class="btn btn-sm btn-primary" hx-get="{% url 'backoffice:product_batch_create' product.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Añadir remesa</button>
</div>
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
<table class="table">
<thead><tr><th>Código</th><th>Cantidad</th><th>Caducidad</th><th>Proveedor</th><th></th></tr></thead>
<tbody>
{% for batch in batches %}
<tr class="hover">
<td>{{ batch.code }}</td>
<td>{{ batch.quantity }}</td>
<td>{{ batch.expiration_date|default:'—' }}</td>
<td>{{ batch.provider|default:'—' }}</td>
<td class="text-right">
<button class="btn btn-ghost btn-xs text-error" hx-get="{% url 'backoffice:product_batch_delete' batch.pk %}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">Borrar</button>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center py-6">Este producto no tiene remesas.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,7 @@
{% extends 'backoffice/base.html' %}
{% block title %}{{ object.name }}{% endblock %}
{% block main %}
{% include 'backoffice/products/_attribute_detail_fragment.html' %}
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'backoffice/base.html' %}
{% block title %}{{ product.name }}{% endblock %}
{% block main %}
{% include 'backoffice/products/_product_detail_fragment.html' %}
{% endblock %}