256 lines
11 KiB
HTML
256 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ sequence.title }}{% endblock %}
|
||
|
||
{% block content %}
|
||
<p class="text-sm text-slate-500"><a href="/sequences" class="hover:underline">← Шаблоны сообщений</a></p>
|
||
<div class="mt-1 flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h1 class="text-2xl font-semibold">{{ sequence.title }}</h1>
|
||
<p class="font-mono text-xs text-slate-500">{{ sequence.key }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{% if saved %}
|
||
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-800">
|
||
{% if saved == 'all' %}Шаблон сохранён целиком.
|
||
{% elif saved == 'meta' %}Параметры шаблона сохранены.
|
||
{% elif saved == 'order' %}Порядок сообщений обновлён.
|
||
{% else %}Сообщение сохранено.{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if errors %}
|
||
<ul class="mt-4 list-disc rounded-lg border border-red-200 bg-red-50 p-3 pl-8 text-sm text-red-700">
|
||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
<form
|
||
id="template-form"
|
||
method="post"
|
||
action="/sequences/{{ sequence.key }}/save"
|
||
enctype="multipart/form-data"
|
||
class="mt-6"
|
||
>
|
||
<input type="hidden" name="csrf_token" value="{{ request.session.get('csrf', '') }}" />
|
||
<input type="hidden" name="item_order" id="item-order" value="{{ item_order }}" />
|
||
|
||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||
<div class="grid gap-3 sm:grid-cols-3">
|
||
<div>
|
||
<label class="text-xs text-slate-500">Название</label>
|
||
<input name="title" value="{{ sequence.title }}"
|
||
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-sky-500 focus:outline-none" />
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<label class="text-xs text-slate-500">Описание</label>
|
||
<input name="description" value="{{ sequence.description }}"
|
||
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-sky-500 focus:outline-none" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 flex items-center justify-between">
|
||
<h2 class="text-lg font-medium">Сообщения ({{ items | length }})</h2>
|
||
<button
|
||
type="button"
|
||
id="add-item"
|
||
class="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||
>
|
||
Добавить сообщение
|
||
</button>
|
||
</div>
|
||
|
||
<div id="items" class="mt-4 space-y-5">
|
||
{% for item in items %}
|
||
<section class="rounded-lg border border-slate-200 bg-white" data-token="{{ item.token }}">
|
||
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-2">
|
||
<div class="text-sm text-slate-600">
|
||
<span class="font-medium" data-position>№ {{ loop.index }}</span>
|
||
{% if item.id %}<span class="ml-2 font-mono text-xs text-slate-400">id {{ item.id }}</span>{% endif %}
|
||
{% if item.enabled %}
|
||
<span class="ml-2 rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">включено</span>
|
||
{% else %}
|
||
<span class="ml-2 rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-500">выключено</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="flex items-center gap-3 text-sm">
|
||
<button type="button" data-action="up" class="text-slate-600 hover:underline">Выше</button>
|
||
<button type="button" data-action="down" class="text-slate-600 hover:underline">Ниже</button>
|
||
<button type="button" data-action="delete" class="text-red-600 hover:underline">Удалить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-4">
|
||
{% with editor_id="text-" ~ item.token, editor_name="text_" ~ item.token, editor_value=item.text, editor_max=(max_caption if item.image_name else max_message), editor_rows=12 %}
|
||
{% include "partials/editor.html" %}
|
||
{% endwith %}
|
||
|
||
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
||
<div>
|
||
<label class="text-xs text-slate-500">Кнопки — по одной в строке: <span class="font-mono">Текст | https://…</span></label>
|
||
<textarea
|
||
name="buttons_{{ item.token }}"
|
||
rows="3"
|
||
placeholder="Канал | https://t.me/…"
|
||
class="mt-1 w-full rounded-lg border border-slate-300 p-2 font-mono text-xs focus:border-sky-500 focus:outline-none"
|
||
>{{ item.buttons_text }}</textarea>
|
||
<p class="mt-1 text-xs text-slate-500">До {{ max_buttons }} кнопок; каждая на своей строке.</p>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500">Картинка (JPEG/PNG/WebP, до 5 МБ)</label>
|
||
{% if item.image_name %}
|
||
<div class="mt-1 flex items-center gap-3">
|
||
<img src="/sequences/media/{{ item.image_name }}" alt="" class="h-20 w-20 rounded object-cover" />
|
||
<label class="flex items-center gap-2 text-xs text-slate-600">
|
||
<input type="checkbox" name="remove_image_{{ item.token }}" /> удалить
|
||
</label>
|
||
</div>
|
||
{% endif %}
|
||
<input type="file" name="image_{{ item.token }}" accept="image/jpeg,image/png,image/webp"
|
||
class="mt-2 block w-full text-xs text-slate-600" />
|
||
<p class="mt-1 text-xs text-slate-500">
|
||
С картинкой текст уходит подписью — не больше {{ max_caption }} символов.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<label class="mt-3 flex items-center gap-2 text-sm text-slate-700">
|
||
<input type="checkbox" name="enabled_{{ item.token }}" {% if item.enabled %}checked{% endif %} /> включено
|
||
</label>
|
||
</div>
|
||
</section>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="mt-6 flex flex-wrap items-center gap-3">
|
||
<button type="submit" class="rounded-lg bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700">
|
||
Сохранить всё
|
||
</button>
|
||
<span class="text-xs text-slate-500">Сохраняет параметры и все сообщения сразу.</span>
|
||
</div>
|
||
</form>
|
||
|
||
<template id="item-template">
|
||
<section class="rounded-lg border border-slate-200 bg-white" data-token="__TOKEN__">
|
||
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-2">
|
||
<div class="text-sm text-slate-600">
|
||
<span class="font-medium" data-position>№</span>
|
||
<span class="ml-2 rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">включено</span>
|
||
</div>
|
||
<div class="flex items-center gap-3 text-sm">
|
||
<button type="button" data-action="up" class="text-slate-600 hover:underline">Выше</button>
|
||
<button type="button" data-action="down" class="text-slate-600 hover:underline">Ниже</button>
|
||
<button type="button" data-action="delete" class="text-red-600 hover:underline">Удалить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-4">
|
||
{% with editor_id="text-__TOKEN__", editor_name="text___TOKEN__", editor_value="", editor_max=max_message, editor_rows=12 %}
|
||
{% include "partials/editor.html" %}
|
||
{% endwith %}
|
||
|
||
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
||
<div>
|
||
<label class="text-xs text-slate-500">Кнопки — по одной в строке: <span class="font-mono">Текст | https://…</span></label>
|
||
<textarea
|
||
name="buttons___TOKEN__"
|
||
rows="3"
|
||
placeholder="Канал | https://t.me/…"
|
||
class="mt-1 w-full rounded-lg border border-slate-300 p-2 font-mono text-xs focus:border-sky-500 focus:outline-none"
|
||
></textarea>
|
||
<p class="mt-1 text-xs text-slate-500">До {{ max_buttons }} кнопок; каждая на своей строке.</p>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500">Картинка (JPEG/PNG/WebP, до 5 МБ)</label>
|
||
<input type="file" name="image___TOKEN__" accept="image/jpeg,image/png,image/webp"
|
||
class="mt-2 block w-full text-xs text-slate-600" />
|
||
<p class="mt-1 text-xs text-slate-500">
|
||
С картинкой текст уходит подписью — не больше {{ max_caption }} символов.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<label class="mt-3 flex items-center gap-2 text-sm text-slate-700">
|
||
<input type="checkbox" name="enabled___TOKEN__" checked /> включено
|
||
</label>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<div class="mt-10">
|
||
<h2 class="text-lg font-medium">История изменений</h2>
|
||
{% if history %}
|
||
<ul class="mt-3 space-y-2">
|
||
{% for change in history %}
|
||
<li class="rounded-lg border border-slate-200 bg-white p-3 text-sm">
|
||
<div class="flex items-center justify-between text-slate-500">
|
||
<span>{{ change.changed_at.strftime("%d.%m.%Y %H:%M") }}</span>
|
||
<span>редактор: {{ change.editor_id or "—" }}</span>
|
||
</div>
|
||
<details class="mt-2">
|
||
<summary class="cursor-pointer text-slate-600">снимок</summary>
|
||
<pre class="mt-1 max-h-64 overflow-auto rounded bg-slate-50 p-2 text-xs">{{ change.snapshot | tojson(indent=2) }}</pre>
|
||
</details>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="mt-2 text-sm text-slate-500">Изменений ещё не было.</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var items = document.getElementById("items");
|
||
var orderInput = document.getElementById("item-order");
|
||
var template = document.getElementById("item-template");
|
||
var addButton = document.getElementById("add-item");
|
||
var counter = 0;
|
||
|
||
function updateOrder() {
|
||
var cards = items.querySelectorAll("[data-token]");
|
||
var tokens = [];
|
||
cards.forEach(function (card, index) {
|
||
tokens.push(card.dataset.token);
|
||
var position = card.querySelector("[data-position]");
|
||
if (position) position.textContent = "№ " + (index + 1);
|
||
});
|
||
orderInput.value = tokens.join(",");
|
||
}
|
||
|
||
addButton.addEventListener("click", function () {
|
||
counter += 1;
|
||
var token = "new_" + counter;
|
||
var wrapper = document.createElement("div");
|
||
wrapper.innerHTML = template.innerHTML.replace(/__TOKEN__/g, token).trim();
|
||
var card = wrapper.firstElementChild;
|
||
items.appendChild(card);
|
||
updateOrder();
|
||
if (window.initEditor) window.initEditor(card);
|
||
var textarea = card.querySelector("textarea");
|
||
if (textarea) textarea.focus();
|
||
});
|
||
|
||
items.addEventListener("click", function (event) {
|
||
var button = event.target.closest("button[data-action]");
|
||
if (!button) return;
|
||
var card = button.closest("[data-token]");
|
||
if (!card) return;
|
||
var action = button.dataset.action;
|
||
if (action === "delete") {
|
||
if (confirm("Удалить сообщение?")) card.remove();
|
||
} else if (action === "up" && card.previousElementSibling) {
|
||
items.insertBefore(card, card.previousElementSibling);
|
||
} else if (action === "down" && card.nextElementSibling) {
|
||
items.insertBefore(card.nextElementSibling, card);
|
||
}
|
||
updateOrder();
|
||
});
|
||
|
||
updateOrder();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|