feat: KoePi mehrere benannte oeffentliche Links statt nur einem, Migration des bestehenden Sina-Links, Name in Logs

This commit is contained in:
2026-07-18 22:47:55 +02:00
parent 7a0b53e894
commit 4c284a257b
6 changed files with 151 additions and 68 deletions

View File

@@ -3312,6 +3312,7 @@ function PushLogs({ toast }) {
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:3, flexWrap:'wrap' }}>
<span style={{ color:'#4ecdc4', fontFamily:"'Space Mono',monospace", fontSize:11, fontWeight:700 }}>
{PUBLIC_LINK_LABELS[l.linkType] || '🔗 Öffentlicher Link besucht'}
{l.linkName && <span style={{ color:'#ffe66d' }}> {l.linkName}</span>}
</span>
<span style={{ display:'flex', alignItems:'center', gap:8 }}>
<span style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>

View File

@@ -262,35 +262,58 @@ export default function Koepi({ toast }) {
finally { setResolvingStores(false); }
};
const [shareInfo, setShareInfo] = useState(null); // { token, url } | null
const [shareLinks, setShareLinks] = useState([]);
const [shareOpen, setShareOpen] = useState(false);
const [shareBusy, setShareBusy] = useState(false);
useEffect(() => {
if (!isAdmin) return;
api('/tools/koepi/share-link').then(setShareInfo).catch(()=>{});
}, [isAdmin]);
const [newLinkName, setNewLinkName] = useState('');
const loadShareLinks = () => {
api('/tools/koepi/share-links').then(r => setShareLinks(r.links || [])).catch(()=>{});
};
useEffect(() => { if (isAdmin) loadShareLinks(); }, [isAdmin]);
const createShareLink = async () => {
const name = newLinkName.trim();
if (!name) { toast?.('Bitte einen Namen eingeben','error'); return; }
setShareBusy(true);
try {
const r = await api('/tools/koepi/share-link', { method:'POST', body:{} });
setShareInfo(r);
toast('🔗 Neuer Link erzeugt — alter Link ist damit ungültig');
await api('/tools/koepi/share-links', { method:'POST', body:{ name } });
setNewLinkName('');
toast(`🔗 Link für "${name}" erstellt`);
loadShareLinks();
} catch(e) { toast?.(e.message||'Fehler','error'); }
finally { setShareBusy(false); }
};
const disableShareLink = async () => {
const resetShareLink = async (id, name) => {
if (!window.confirm(`Link für "${name}" wirklich zurücksetzen? Der alte Link funktioniert danach nicht mehr.`)) return;
setShareBusy(true);
try {
await api('/tools/koepi/share-link/disable', { method:'POST', body:{} });
setShareInfo({ token:null, url:null });
toast('🔗 Link deaktiviert');
await api(`/tools/koepi/share-links/${id}/reset`, { method:'POST', body:{} });
toast(`🔄 Link für "${name}" zurückgesetzt`);
loadShareLinks();
} catch(e) { toast?.(e.message||'Fehler','error'); }
finally { setShareBusy(false); }
};
const copyShareLink = async () => {
if (!shareInfo?.url) return;
try { await navigator.clipboard.writeText(shareInfo.url); toast('Link kopiert'); }
catch { toast?.('Kopieren nicht möglich — bitte manuell markieren','error'); }
const deleteShareLink = async (id, name) => {
if (!window.confirm(`Link für "${name}" wirklich löschen?`)) return;
setShareBusy(true);
try {
await api(`/tools/koepi/share-links/${id}`, { method:'DELETE' });
toast(`🗑 Link für "${name}" gelöscht`);
loadShareLinks();
} catch(e) { toast?.(e.message||'Fehler','error'); }
finally { setShareBusy(false); }
};
const copyLink = async (url) => {
try { await navigator.clipboard.writeText(url); toast('Link kopiert'); return; } catch {}
try {
const ta = document.createElement('textarea');
ta.value = url; ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
toast?.(ok ? 'Link kopiert' : 'Kopieren nicht möglich', ok ? 'success' : 'error');
} catch { toast?.('Kopieren nicht möglich','error'); }
};
return (
@@ -310,37 +333,52 @@ export default function Koepi({ toast }) {
{resolvingStores ? '⏳ läuft…' : '🏪 Filial-Liste neu auflösen'}
</button>
<button onClick={clearCache} style={{ ...S.btn('#666666',true), fontSize:10, flexShrink:0, whiteSpace:'nowrap' }}>🗑 Cache leeren</button>
<button onClick={()=>setShareOpen(v=>!v)} style={{ ...S.btn(shareInfo?.token?'#ffe66d':'#888888',true), fontSize:10, flexShrink:0, whiteSpace:'nowrap' }}>
🔗 Teilen{shareInfo?.token ? ' (aktiv)' : ''}
<button onClick={()=>setShareOpen(v=>!v)} style={{ ...S.btn(shareLinks.length?'#ffe66d':'#888888',true), fontSize:10, flexShrink:0, whiteSpace:'nowrap' }}>
🔗 Teilen{shareLinks.length ? ` (${shareLinks.length})` : ''}
</button>
</div>
)}
{isAdmin && shareOpen && (
<div style={{ ...S.card, marginTop:10 }}>
<div style={{ ...S.sub, marginBottom:10 }}>
Öffentlicher, schreibgeschützter Link ohne Login zeigt zuerst die Prospekte, man kann dort
auch zu den Angeboten wechseln. Keine Admin-Funktionen sichtbar/erreichbar über diesen Link.
Öffentliche, schreibgeschützte Links ohne Login zeigen zuerst die Prospekte, man kann dort
auch zu den Angeboten wechseln. Keine Admin-Funktionen sichtbar/erreichbar über diese Links.
Du kannst mehrere Links gleichzeitig anlegen (z.B. einen pro Person) in den Logs siehst du
dann, wer welchen Link benutzt hat.
</div>
{shareInfo?.token ? (
<>
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
<input readOnly value={shareInfo.url} onClick={e=>e.target.select()} style={{ ...S.inp, flex:1, minWidth:200, fontSize:11 }}/>
<button onClick={copyShareLink} style={{ ...S.btn('#4ecdc4') }}>📋 Kopieren</button>
</div>
<div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
<button onClick={createShareLink} disabled={shareBusy} style={{ ...S.btn('#f59e0b'), opacity:shareBusy?0.5:1 }}>
{shareBusy?'…':'🔄 Neuen Link erzeugen (alter wird ungültig)'}
</button>
<button onClick={disableShareLink} disabled={shareBusy} style={{ ...S.btn('#f87171'), opacity:shareBusy?0.5:1 }}>
{shareBusy?'…':'🚫 Link deaktivieren'}
</button>
</div>
</>
) : (
<button onClick={createShareLink} disabled={shareBusy} style={{ ...S.btn('#4ecdc4'), opacity:shareBusy?0.5:1 }}>
{shareBusy?'…':'🔗 Link erzeugen'}
</button>
{shareLinks.length > 0 && (
<div style={{ display:'flex', flexDirection:'column', gap:8, marginBottom:14 }}>
{shareLinks.map(l => (
<div key={l.id} style={{ border:'1px solid rgba(255,255,255,0.08)', borderRadius:8, padding:'8px 10px' }}>
<div style={{ color:'#ffe66d', fontFamily:"'Space Mono',monospace", fontSize:12, fontWeight:700, marginBottom:4 }}>
{l.name}
</div>
<div style={{ display:'flex', gap:8, marginBottom:6, flexWrap:'wrap' }}>
<input readOnly value={l.url} onClick={e=>e.target.select()} style={{ ...S.inp, flex:1, minWidth:180, fontSize:10 }}/>
<button onClick={()=>copyLink(l.url)} style={{ ...S.btn('#4ecdc4'), fontSize:11 }}>📋</button>
</div>
<div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
<button onClick={()=>resetShareLink(l.id, l.name)} disabled={shareBusy} style={{ ...S.btn('#f59e0b'), fontSize:10, opacity:shareBusy?0.5:1 }}>
🔄 Zurücksetzen
</button>
<button onClick={()=>deleteShareLink(l.id, l.name)} disabled={shareBusy} style={{ ...S.btn('#f87171'), fontSize:10, opacity:shareBusy?0.5:1 }}>
🗑 Löschen
</button>
</div>
</div>
))}
</div>
)}
<div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
<input value={newLinkName} onChange={e=>setNewLinkName(e.target.value)}
onKeyDown={e=>e.key==='Enter' && createShareLink()}
placeholder="Name (z.B. Sina)" style={{ ...S.inp, flex:1, minWidth:140 }}/>
<button onClick={createShareLink} disabled={shareBusy || !newLinkName.trim()} style={{ ...S.btn('#4ecdc4'), opacity:(shareBusy || !newLinkName.trim())?0.5:1 }}>
{shareBusy?'…':' Neuen Link erstellen'}
</button>
</div>
</div>
)}
</div>