Improved features, implemented moco integration
This commit is contained in:
@@ -52,6 +52,7 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-brand-bg-light">
|
||||
{% for key in keys %}
|
||||
{% set key_offers = offers_by_key.get(key.id | string, []) %}
|
||||
<tr class="hover:bg-brand-bg/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-mono text-white font-medium">{{ key.key }}</div>
|
||||
@@ -67,44 +68,42 @@
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if key.used %}
|
||||
<span class="px-3 py-1 bg-gray-600/30 text-gray-400 rounded-lg text-sm">
|
||||
Verwendet
|
||||
</span>
|
||||
<span class="px-3 py-1 bg-gray-600/30 text-gray-400 rounded-lg text-sm">Verwendet</span>
|
||||
{% else %}
|
||||
<span class="px-3 py-1 bg-green-600/30 text-green-400 rounded-lg text-sm">
|
||||
Verfügbar
|
||||
</span>
|
||||
<span class="px-3 py-1 bg-green-600/30 text-green-400 rounded-lg text-sm">Verfügbar</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-400">
|
||||
{{ key.created_at.strftime('%d.%m.%Y') if key.created_at else '-' }}
|
||||
{% if key_offers %}
|
||||
<button onclick="toggleOffers('{{ key.id }}')"
|
||||
class="ml-2 px-2 py-0.5 bg-brand-highlight/20 text-brand-highlight rounded text-xs font-medium hover:bg-brand-highlight/30 transition-colors">
|
||||
📄 {{ key_offers | length }} Angebot{{ 'e' if key_offers | length != 1 else '' }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button onclick="editKey('{{ key.id }}', {{ key.max_employees }}, {{ key.daily_token_limit or '' }}, '{{ key.description or '' }}')"
|
||||
class="text-blue-400 hover:text-blue-300 p-2 rounded transition-colors"
|
||||
title="Bearbeiten">
|
||||
class="text-blue-400 hover:text-blue-300 p-2 rounded transition-colors" title="Bearbeiten">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="openOfferModal('{{ key.id }}', {{ key.max_employees }}, {{ key.daily_token_limit or 0 }}, '{{ key.description or '' }}')"
|
||||
class="text-green-400 hover:text-green-300 p-2 rounded transition-colors"
|
||||
title="Angebot senden">
|
||||
class="text-green-400 hover:text-green-300 p-2 rounded transition-colors" title="Angebot in MOCO erstellen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% if not key.used %}
|
||||
<button onclick="copyKey('{{ key.key }}')"
|
||||
class="text-brand-highlight hover:text-brand-highlight-dark p-2 rounded transition-colors"
|
||||
title="Kopieren">
|
||||
class="text-brand-highlight hover:text-brand-highlight-dark p-2 rounded transition-colors" title="Kopieren">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteKey('{{ key.id }}')"
|
||||
class="text-red-400 hover:text-red-300 p-2 rounded transition-colors"
|
||||
title="Löschen">
|
||||
class="text-red-400 hover:text-red-300 p-2 rounded transition-colors" title="Löschen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
@@ -112,6 +111,41 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if key_offers %}
|
||||
<tr id="offers-{{ key.id }}" class="hidden bg-brand-bg/20">
|
||||
<td colspan="5" class="px-6 py-3">
|
||||
<div class="space-y-2">
|
||||
{% for offer in key_offers %}
|
||||
<div class="flex items-center justify-between bg-brand-bg rounded-lg px-4 py-2.5 border border-brand-bg-light">
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<a href="{{ offer.moco_offer_url }}" target="_blank"
|
||||
class="font-medium text-white hover:text-brand-highlight transition-colors">
|
||||
{{ offer.moco_offer_identifier or offer.offer_title or 'Angebot' }}
|
||||
</a>
|
||||
<span class="text-gray-400">{{ offer.company_name }}</span>
|
||||
<span class="text-gray-400">
|
||||
{% if offer.price %}{{ "%.2f"|format(offer.price) }} €{% endif %}
|
||||
{% if offer.payment_frequency %} / {{ offer.payment_frequency }}{% endif %}
|
||||
</span>
|
||||
<span class="text-gray-500 text-xs">{{ offer.created_at.strftime('%d.%m.%Y') if offer.created_at else '' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if offer.status == 'sent' %}
|
||||
<span class="px-2 py-0.5 bg-green-600/30 text-green-400 rounded text-xs">Versendet</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-0.5 bg-yellow-600/30 text-yellow-400 rounded text-xs">Entwurf</span>
|
||||
<button onclick="openSendModal('{{ offer.id }}', '{{ offer.moco_offer_identifier or offer.offer_title }}', '{{ offer.company_name }}')"
|
||||
class="px-3 py-1 bg-brand-highlight text-brand-bg rounded text-xs font-semibold hover:bg-brand-highlight-dark transition-colors">
|
||||
Versenden
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -166,13 +200,14 @@
|
||||
<!-- Offer Modal -->
|
||||
<div id="offerModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="card-bg rounded-xl border p-8 max-w-lg w-full mx-4">
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Angebot senden</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">Erstellt ein professionelles PDF-Angebot und sendet es per E-Mail.</p>
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Angebot in MOCO erstellen</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">Erstellt ein Angebot direkt in MOCO. Versand erfolgt manuell in MOCO.</p>
|
||||
|
||||
<form id="offerForm" class="space-y-4">
|
||||
<input type="hidden" id="offer_key_id" name="key_id">
|
||||
<input type="hidden" id="offer_company_name_hidden" name="company_name">
|
||||
|
||||
<!-- Calculated Info -->
|
||||
<!-- Plan Info -->
|
||||
<div id="offerCalcInfo" class="bg-brand-bg/60 rounded-lg p-4 text-sm space-y-1 border border-gray-600">
|
||||
<div class="flex justify-between"><span class="text-gray-400">Plan:</span><span id="offer_plan" class="text-white font-medium"></span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-400">Mitarbeiter:</span><span id="offer_employees" class="text-white"></span></div>
|
||||
@@ -181,6 +216,16 @@
|
||||
<div class="border-t border-gray-600 pt-2 mt-2 flex justify-between"><span class="text-gray-400">Empfohlener Preis:</span><span id="offer_suggested" class="text-brand-highlight font-bold"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Company Dropdown -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Firma (MOCO)</label>
|
||||
<select id="offer_company_select" name="company_id" required
|
||||
onchange="onCompanySelect(this)"
|
||||
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white focus:border-brand-highlight focus:outline-none">
|
||||
<option value="">Wird geladen…</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Angebotspreis (€/Monat)</label>
|
||||
@@ -213,20 +258,13 @@
|
||||
Jährlicher Gesamtbetrag: <span id="yearly-total" class="text-white font-semibold"></span>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">E-Mail-Adresse</label>
|
||||
<input type="email" id="offer_email" name="email" placeholder="kunde@beispiel.de" required
|
||||
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white focus:border-brand-highlight focus:outline-none">
|
||||
</div>
|
||||
|
||||
<div id="offer-status" class="hidden text-sm rounded-lg px-4 py-3"></div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit" id="offerSubmitBtn"
|
||||
class="flex-1 px-6 py-3 btn-primary rounded-lg font-medium flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
Angebot senden
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Angebot in MOCO erstellen
|
||||
</button>
|
||||
<button type="button" onclick="closeOfferModal()"
|
||||
class="px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors">
|
||||
@@ -237,6 +275,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Offer Confirmation Modal -->
|
||||
<div id="sendModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="card-bg rounded-xl border p-8 max-w-md w-full mx-4">
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Angebot versenden</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">Das Angebot wird per E-Mail an die hinterlegten Empfänger in MOCO gesendet.</p>
|
||||
|
||||
<div id="sendOfferInfo" class="bg-brand-bg/60 rounded-lg p-4 text-sm space-y-1 border border-gray-600 mb-6">
|
||||
<div class="flex justify-between"><span class="text-gray-400">Angebot:</span><span id="send_offer_title" class="text-white font-medium"></span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-400">Firma:</span><span id="send_offer_company" class="text-white"></span></div>
|
||||
</div>
|
||||
|
||||
<div id="send-status" class="hidden text-sm rounded-lg px-4 py-3 mb-4"></div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button id="sendConfirmBtn" onclick="confirmSendOffer()"
|
||||
class="flex-1 px-6 py-3 btn-primary rounded-lg font-medium flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
Jetzt versenden
|
||||
</button>
|
||||
<button type="button" onclick="closeSendModal()"
|
||||
class="px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Modal -->
|
||||
<div id="generateModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="card-bg rounded-xl border p-8 max-w-md w-full mx-4">
|
||||
@@ -374,7 +439,7 @@ async function deleteKey(keyId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Offer Modal ────────────────────────────────────────────────────────────
|
||||
// ── Offer Modal (MOCO) ─────────────────────────────────────────────────────
|
||||
const AVG_TOKENS_PER_POST = 50000;
|
||||
const API_COST_PER_1K_EUR = 0.003;
|
||||
const SERVER_SHARE_EUR = 1.60;
|
||||
@@ -384,12 +449,10 @@ function calcSuggestedPrice(dailyTokenLimit) {
|
||||
const monthlyTokens = dailyTokenLimit * 30;
|
||||
const apiCostEur = (monthlyTokens / 1000) * API_COST_PER_1K_EUR;
|
||||
const raw = apiCostEur * 2.5 + SERVER_SHARE_EUR * 2 + 5;
|
||||
return Math.ceil(raw / 5) * 5; // round up to nearest 5€
|
||||
return Math.ceil(raw / 5) * 5;
|
||||
}
|
||||
|
||||
function fmtNum(n) {
|
||||
return n.toLocaleString('de-DE');
|
||||
}
|
||||
function fmtNum(n) { return n.toLocaleString('de-DE'); }
|
||||
|
||||
function openOfferModal(keyId, maxEmployees, dailyTokenLimit, description) {
|
||||
document.getElementById('offer_key_id').value = keyId;
|
||||
@@ -409,8 +472,13 @@ function openOfferModal(keyId, maxEmployees, dailyTokenLimit, description) {
|
||||
document.getElementById('offer_suggested').textContent = '–';
|
||||
}
|
||||
|
||||
// Reset company select
|
||||
const sel = document.getElementById('offer_company_select');
|
||||
sel.innerHTML = '<option value="">Wird geladen…</option>';
|
||||
document.getElementById('offer_company_name_hidden').value = '';
|
||||
loadCompanies();
|
||||
|
||||
selectFreq('monatlich');
|
||||
document.getElementById('offer_email').value = '';
|
||||
document.getElementById('offer-status').classList.add('hidden');
|
||||
document.getElementById('offerModal').classList.remove('hidden');
|
||||
}
|
||||
@@ -423,10 +491,8 @@ function closeOfferModal() {
|
||||
function selectFreq(freq) {
|
||||
document.getElementById('offer_payment_frequency').value = freq;
|
||||
['monatlich', 'jährlich', 'einmalig'].forEach(f => {
|
||||
const btn = document.getElementById('freq-' + f);
|
||||
btn.classList.toggle('active-freq', f === freq);
|
||||
document.getElementById('freq-' + f).classList.toggle('active-freq', f === freq);
|
||||
});
|
||||
// Show yearly total hint
|
||||
const price = parseFloat(document.getElementById('offer_price').value) || 0;
|
||||
const hint = document.getElementById('yearly-hint');
|
||||
if (freq === 'jährlich' && price > 0) {
|
||||
@@ -438,43 +504,141 @@ function selectFreq(freq) {
|
||||
}
|
||||
|
||||
document.getElementById('offer_price').addEventListener('input', () => {
|
||||
if (document.getElementById('offer_payment_frequency').value === 'jährlich') {
|
||||
selectFreq('jährlich');
|
||||
}
|
||||
if (document.getElementById('offer_payment_frequency').value === 'jährlich') selectFreq('jährlich');
|
||||
});
|
||||
|
||||
// ── Company select ─────────────────────────────────────────────────────────
|
||||
async function loadCompanies() {
|
||||
const sel = document.getElementById('offer_company_select');
|
||||
try {
|
||||
const resp = await fetch('/admin/api/moco/companies');
|
||||
if (!resp.ok) throw new Error('Fehler beim Laden');
|
||||
const companies = await resp.json();
|
||||
sel.innerHTML = '<option value="">Firma auswählen…</option>';
|
||||
companies.forEach(c => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.id;
|
||||
opt.textContent = c.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
sel.innerHTML = '<option value="">Fehler beim Laden der Firmen</option>';
|
||||
console.error('Company load error', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onCompanySelect(sel) {
|
||||
const name = sel.options[sel.selectedIndex]?.text || '';
|
||||
document.getElementById('offer_company_name_hidden').value = sel.value ? name : '';
|
||||
}
|
||||
|
||||
document.getElementById('offerForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const keyId = formData.get('key_id');
|
||||
const companyId = formData.get('company_id');
|
||||
const statusEl = document.getElementById('offer-status');
|
||||
const submitBtn = document.getElementById('offerSubmitBtn');
|
||||
|
||||
if (!companyId) {
|
||||
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-red-900/40 border border-red-600 text-red-300';
|
||||
statusEl.textContent = 'Bitte eine Firma auswählen.';
|
||||
statusEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird gesendet…';
|
||||
submitBtn.textContent = 'Wird erstellt…';
|
||||
statusEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/license-keys/${keyId}/send-offer`, {
|
||||
const response = await fetch(`/admin/api/license-keys/${keyId}/create-offer`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) throw new Error(data.detail || 'Fehler beim Senden');
|
||||
if (!response.ok) throw new Error(data.detail || 'Fehler beim Erstellen');
|
||||
|
||||
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-green-900/40 border border-green-600 text-green-300';
|
||||
statusEl.textContent = data.message || 'Angebot erfolgreich gesendet!';
|
||||
statusEl.innerHTML = 'Angebot erfolgreich erstellt! '
|
||||
+ (data.offer_url ? `<a href="${data.offer_url}" target="_blank" class="underline font-semibold text-green-300 hover:text-white">In MOCO öffnen →</a>` : '');
|
||||
statusEl.classList.remove('hidden');
|
||||
submitBtn.textContent = '✓ Gesendet';
|
||||
submitBtn.textContent = '✓ Erstellt';
|
||||
// Reset button after 4 seconds so another offer can be created
|
||||
setTimeout(() => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> Angebot in MOCO erstellen';
|
||||
location.reload();
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-red-900/40 border border-red-600 text-red-300';
|
||||
statusEl.textContent = 'Fehler: ' + error.message;
|
||||
statusEl.classList.remove('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg> Angebot senden';
|
||||
submitBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> Angebot in MOCO erstellen';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Offer sub-rows toggle ───────────────────────────────────────────────────
|
||||
function toggleOffers(keyId) {
|
||||
const row = document.getElementById('offers-' + keyId);
|
||||
if (!row) return;
|
||||
row.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// ── Send Offer Modal ────────────────────────────────────────────────────────
|
||||
let currentSendOfferId = null;
|
||||
|
||||
function openSendModal(offerId, title, company) {
|
||||
currentSendOfferId = offerId;
|
||||
document.getElementById('send_offer_title').textContent = title || 'Angebot';
|
||||
document.getElementById('send_offer_company').textContent = company || '–';
|
||||
const statusEl = document.getElementById('send-status');
|
||||
statusEl.classList.add('hidden');
|
||||
statusEl.textContent = '';
|
||||
const btn = document.getElementById('sendConfirmBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg> Jetzt versenden';
|
||||
document.getElementById('sendModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeSendModal() {
|
||||
document.getElementById('sendModal').classList.add('hidden');
|
||||
currentSendOfferId = null;
|
||||
}
|
||||
|
||||
async function confirmSendOffer() {
|
||||
if (!currentSendOfferId) return;
|
||||
const statusEl = document.getElementById('send-status');
|
||||
const btn = document.getElementById('sendConfirmBtn');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird versendet…';
|
||||
statusEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/license-key-offers/${currentSendOfferId}/send`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.detail || 'Fehler beim Versenden');
|
||||
|
||||
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-green-900/40 border border-green-600 text-green-300';
|
||||
statusEl.textContent = 'Angebot erfolgreich versendet!';
|
||||
statusEl.classList.remove('hidden');
|
||||
btn.textContent = '✓ Versendet';
|
||||
setTimeout(() => {
|
||||
closeSendModal();
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-red-900/40 border border-red-600 text-red-300';
|
||||
statusEl.textContent = 'Fehler: ' + error.message;
|
||||
statusEl.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg> Erneut versuchen';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -705,8 +705,8 @@
|
||||
<div id="editView" class="hidden">
|
||||
<textarea id="editTextarea" class="edit-textarea">{{ post.post_content }}</textarea>
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<span class="text-sm text-gray-400">
|
||||
<span id="charCount">{{ post.post_content | length }}</span> Zeichen
|
||||
<span class="text-sm" id="charCountLabel">
|
||||
<span id="charCount">{{ post.post_content | length }}</span> / 3000 Zeichen
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="cancelEdit()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light text-gray-300 rounded-lg transition-colors">
|
||||
@@ -1137,7 +1137,16 @@ function setPreviewMode(mode) {
|
||||
function updateCharCount() {
|
||||
const textarea = document.getElementById('editTextarea');
|
||||
const charCount = document.getElementById('charCount');
|
||||
charCount.textContent = textarea.value.length;
|
||||
const label = document.getElementById('charCountLabel');
|
||||
const len = textarea.value.length;
|
||||
charCount.textContent = len;
|
||||
if (len > 3000) {
|
||||
label.style.color = '#ef4444';
|
||||
} else if (len > 2700) {
|
||||
label.style.color = '#f59e0b';
|
||||
} else {
|
||||
label.style.color = '#9ca3af';
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
@@ -1154,6 +1163,11 @@ async function saveEdit() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newContent.length > 3000) {
|
||||
showToast('Post überschreitet das LinkedIn-Limit von 3000 Zeichen.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const originalBtnHTML = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<div class="loading-spinner"></div>';
|
||||
@@ -1979,10 +1993,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize media upload (multi-media support)
|
||||
initMediaUpload();
|
||||
|
||||
// Add event listener for textarea character count
|
||||
// Add event listener for textarea character count + initialize color
|
||||
const textarea = document.getElementById('editTextarea');
|
||||
if (textarea) {
|
||||
textarea.addEventListener('input', updateCharCount);
|
||||
updateCharCount();
|
||||
}
|
||||
|
||||
// Allow Enter key to apply custom suggestion
|
||||
|
||||
Reference in New Issue
Block a user