discount profiles and discounts overall implemented and working
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-21 23:46:37 +02:00
parent c713627a2c
commit 1df8d66d5d
27 changed files with 1317 additions and 89 deletions

View File

@@ -82,6 +82,10 @@
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-discounts", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/catalog/discount-profiles" data-nav="/admin/catalog/discount-profiles"
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-discount-profiles", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/catalog/categories" data-nav="/admin/catalog/categories"
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-categories", lang=lang | default(value='sk')) }}

View File

@@ -0,0 +1,71 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{% if profile %}{{ t(key="edit-profile", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-profile", lang=lang | default(value='sk')) }}{% endif %}{% endblock title %}
{% block crumb %}{{ t(key="admin-discount-profiles", lang=lang | default(value='sk')) }}{% endblock crumb %}
{% block content %}
<div class="flex items-center justify-between gap-3">
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{% if profile %}{{ t(key="edit-profile", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-profile", lang=lang | default(value='sk')) }}{% endif %}
</h1>
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/discount-profiles", size="px-3 py-2 text-sm") }}
</div>
{% if profile %}{% set v_name = profile.name %}{% set v_percent = profile.percent %}{% set v_scope = profile.scope_type %}
{% else %}{% set v_name = "" %}{% set v_percent = "" %}{% set v_scope = "include" %}{% endif %}
<form method="post"
action="{% if profile %}/admin/catalog/discount-profiles/{{ profile.id }}{% else %}/admin/catalog/discount-profiles{% endif %}"
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
{{ ui::csrf_field() }}
{% if error %}{{ ui::alert_danger(message=t(key=error, lang=lang | default(value='sk'))) }}{% endif %}
<div class="grid gap-5 sm:grid-cols-2">
<div class="space-y-1.5">
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="name", id="name", required=true, value=v_name) }}
</div>
<div class="space-y-1.5">
<label for="percent" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="discount-percent", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="percent", id="percent", required=true, value=v_percent, placeholder="0", attrs='inputmode="decimal" min="0" max="100"') }}
</div>
</div>
<fieldset class="space-y-2">
<legend class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="scope", lang=lang | default(value='sk')) }}</legend>
<label class="flex items-center gap-2 text-sm text-on-surface dark:text-on-surface-dark">
<input type="radio" name="scope_type" value="include" {% if v_scope != "all_except" %}checked{% endif %}>
{{ t(key="scope-include-hint", lang=lang | default(value='sk')) }}
</label>
<label class="flex items-center gap-2 text-sm text-on-surface dark:text-on-surface-dark">
<input type="radio" name="scope_type" value="all_except" {% if v_scope == "all_except" %}checked{% endif %}>
{{ t(key="scope-all-except-hint", lang=lang | default(value='sk')) }}
</label>
</fieldset>
<div class="space-y-1.5">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="products", lang=lang | default(value='sk')) }}</span>
<div class="max-h-72 overflow-y-auto rounded-radius border border-outline p-3 dark:border-outline-dark">
{% if products | length > 0 %}
<div class="grid gap-2 sm:grid-cols-2">
{% for product in products %}
<label class="flex items-center gap-2 text-sm text-on-surface dark:text-on-surface-dark">
<input type="checkbox" name="product_ids" value="{{ product.id }}" {% if product.selected %}checked{% endif %}>
{{ product.name }}
</label>
{% endfor %}
</div>
{% else %}
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="admin-no-products", lang=lang | default(value='sk')) }}</p>
{% endif %}
</div>
</div>
<div class="flex gap-3 pt-2">
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/discount-profiles") }}
</div>
</form>
{% endblock content %}

View File

@@ -0,0 +1,58 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="admin-discount-profiles", lang=lang | default(value='sk')) }}{% endblock title %}
{% block crumb %}{{ t(key="admin-discount-profiles", lang=lang | default(value='sk')) }}{% endblock crumb %}
{% block content %}
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-discount-profiles", lang=lang | default(value='sk')) }}</h1>
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-discount-profiles-desc", lang=lang | default(value='sk')) }}</p>
</div>
{{ ui::button(label=t(key="new-profile", lang=lang | default(value='sk')), href="/admin/catalog/discount-profiles/new") }}
</div>
<div class="mt-6 {{ ui::table_wrap_cls() }}">
{% if profiles | length > 0 %}
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
{{ ui::th(label=t(key="name", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="discount-percent", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="scope", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="products", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
</tr>
</thead>
<tbody class="{{ ui::tbody_cls() }}">
{% for profile in profiles %}
<tr class="{{ ui::row_cls() }}">
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ profile.name }}</td>
<td class="px-4 py-3 tabular-nums">{{ profile.percent }}%</td>
<td class="px-4 py-3">
{% if profile.scope_type == "all_except" %}{{ t(key="scope-all-except", lang=lang | default(value='sk')) }}{% else %}{{ t(key="scope-include", lang=lang | default(value='sk')) }}{% endif %}
</td>
<td class="px-4 py-3 tabular-nums">{{ profile.product_count }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap justify-end gap-2">
{{ ui::button(variant="outline-secondary", label=t(key="edit", lang=lang | default(value='sk')), href="/admin/catalog/discount-profiles/" ~ profile.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
<form method="post" action="/admin/catalog/discount-profiles/{{ profile.id }}/delete"
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
{{ ui::csrf_field() }}
{{ ui::button(variant="outline-danger", label=t(key="delete", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-profiles", lang=lang | default(value='sk')) }}</p>
{{ ui::button(label=t(key="new-profile", lang=lang | default(value='sk')), href="/admin/catalog/discount-profiles/new") }}
</div>
{% endif %}
</div>
{% endblock content %}

View File

@@ -17,15 +17,40 @@
<div class="mt-4">{{ ui::alert_danger(message=t(key=error, lang=lang | default(value='sk'))) }}</div>
{% endif %}
<p class="mt-4 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="negotiated-prices-hint", lang=lang | default(value='sk')) }}</p>
<!-- assigned discount profiles -->
<section class="mt-6 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<h2 class="text-lg font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="discount-profiles", lang=lang | default(value='sk')) }}</h2>
{% if profiles | length > 0 %}
<form method="post" action="/admin/customers/{{ customer.id }}/profiles" class="mt-3 space-y-3">
{{ ui::csrf_field() }}
<div class="grid gap-2 sm:grid-cols-2">
{% for profile in profiles %}
<label class="flex items-center gap-2 text-sm text-on-surface dark:text-on-surface-dark">
<input type="checkbox" name="profile_ids" value="{{ profile.id }}" {% if profile.assigned %}checked{% endif %}>
<span>{{ profile.name }} <span class="text-on-surface/60 dark:text-on-surface-dark/60">({{ profile.percent }}%, {% if profile.scope_type == "all_except" %}{{ t(key="scope-all-except", lang=lang | default(value='sk')) }}{% else %}{{ t(key="scope-include", lang=lang | default(value='sk')) }}{% endif %})</span></span>
</label>
{% endfor %}
</div>
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", size="px-4 py-2 text-sm") }}
</form>
{% else %}
<p class="mt-2 text-sm text-on-surface/70 dark:text-on-surface-dark/70">
{{ t(key="admin-no-profiles", lang=lang | default(value='sk')) }}
<a href="/admin/catalog/discount-profiles/new" class="text-primary dark:text-primary-dark">{{ t(key="new-profile", lang=lang | default(value='sk')) }}</a>
</p>
{% endif %}
</section>
<div class="mt-4 {{ ui::table_wrap_cls() }}">
<p class="mt-6 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="negotiated-prices-hint", lang=lang | default(value='sk')) }}</p>
<div class="mt-3 {{ ui::table_wrap_cls() }}">
{% if products | length > 0 %}
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="public-price", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="automated-price", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="negotiated-price", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="effective-price", lang=lang | default(value='sk')), align="text-right") }}
</tr>
@@ -42,6 +67,27 @@
{{ product.public_price }} {{ product.currency }}
{% endif %}
</td>
<td class="px-4 py-3 tabular-nums">
{% if product.auto_price %}
<div>{{ product.auto_price }} {{ product.currency }}</div>
{% if product.collision %}
<div class="mt-1">{{ ui::badge(label=t(key="collision", lang=lang | default(value='sk')), variant="warning") }}</div>
<form method="post" action="/admin/customers/{{ customer.id }}/resolutions/{{ product.product_id }}" class="mt-1 flex items-center gap-1">
{{ ui::csrf_field() }}
<select name="profile_id" class="rounded-radius border border-outline bg-surface-alt px-2 py-1 text-xs dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark">
{% for c in product.covering %}
<option value="{{ c.id }}" {% if c.id == product.auto_profile_id %}selected{% endif %}>{{ c.name }}</option>
{% endfor %}
</select>
{{ ui::button(label=t(key="resolve", lang=lang | default(value='sk')), type="submit", size="px-2 py-1 text-xs") }}
</form>
{% elif product.auto_profile_name %}
<div class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ product.auto_profile_name }}</div>
{% endif %}
{% else %}
<span class="text-on-surface/40 dark:text-on-surface-dark/40"></span>
{% endif %}
</td>
<td class="px-4 py-3">
<form method="post" action="/admin/customers/{{ customer.id }}/prices/{{ product.product_id }}" class="flex items-center gap-2">
{{ ui::csrf_field() }}