diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index 6b9de37..79e1e31 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -99,6 +99,9 @@ export const DOCUMENTS = [ { key: "leistungsbeschreibung-terminplaner", title: "Leistungsbeschreibung Terminplaner", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-terminplaner.html" }, { key: "avv-terminplaner", title: "AVV Terminplaner (Auftragsverarbeitung)", liveUrl: "https://project.webklar.com/rechtliches/avv-terminplaner.html" }, { key: "widerrufsbelehrung-funktionen", title: "Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)", liveUrl: "https://project.webklar.com/rechtliches/widerrufsbelehrung-funktionen.html" }, + // Webshop: gleiches Muster wie beim Terminplaner (Alias-Seite ohne Fassungsdatum) + { key: "leistungsbeschreibung-webshop", title: "Leistungsbeschreibung Webshop", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop.html" }, + { key: "avv-webshop", title: "AVV Webshop", liveUrl: "https://project.webklar.com/rechtliches/avv-webshop.html" }, // Oeffnungszeiten live: Schluessel hoechstens 36 Zeichen (WOMS-Grenze fuer Dokument-IDs); die Fassungen (PDF/HTML) laedt der Inhaber hoch { key: "leistungsbeschreibung-zeiten", title: "Leistungsbeschreibung Öffnungszeiten live", liveUrl: "https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html" }, ].map((d, i) => ({ ...d, sort: i + 1 })); diff --git a/src/components/TicketProjects.jsx b/src/components/TicketProjects.jsx index 7d94978..e1c395e 100644 --- a/src/components/TicketProjects.jsx +++ b/src/components/TicketProjects.jsx @@ -1,28 +1,13 @@ import { useState, useEffect, useCallback } from 'react' import { FaPlus, FaLink } from 'react-icons/fa6' import { useWebsiteProjects } from '../hooks/useWebsiteProjects' -import { createProjectFromTemplate, importNewRepos } from '../lib/projectAdminApi' -import { isOutsideOrg, assignSummary } from '../lib/projectAssign' +import { createProjectFromTemplate, importNewReposThrottled } from '../lib/projectAdminApi' +import { isOutsideOrg, assignSummary, assignableProjects } from '../lib/projectAssign' +import { slugify } from '../lib/subdomain' import AssignedProjectCard from './AssignedProjectCard' import { fetchServices, completeAssignment } from '../lib/projectKindsApi' import { kindLabelFor } from '../lib/assignFlow' -function slugify(v) { - return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100) -} - -// Zuordnen lassen sich echte Repos des gleichen Kunden oder ohne Kunden, die nicht -// archiviert, keine Vorlage und noch nicht an diesem Ticket sind -function assignableProjects(all, ticket, assignedIds) { - return (all || []).filter((p) => - p.repoFullName && - !assignedIds.has(p.$id) && - p.status !== 'archived' && - !p.isTemplate && - (!p.customerId || p.customerId === ticket.customerId) - ) -} - // Projekte am Ticket: Liste, Zuweisen, Loesen und neues Projekt an einer Stelle. // Loesen nimmt das Projekt nur vom Ticket; die Kundenzuordnung (Sichtbarkeit im Portal) bleibt. export default function TicketProjects({ ticket }) { @@ -57,7 +42,7 @@ export default function TicketProjects({ ticket }) { setImporting(true) setImportHint('') try { - await importNewRepos() + await importNewReposThrottled() } catch (err) { if (!cancelled) setImportHint(`Neue Repos konnten nicht geladen werden (${err.message}).`) } finally { @@ -73,10 +58,11 @@ export default function TicketProjects({ ticket }) { setError('') try { await action() - await load() } catch (err) { setError(err.message || 'Aktion fehlgeschlagen') } finally { + // Auch nach einem Fehler neu laden: das Repo kann trotzdem entstanden sein + try { await load() } catch { /* Anzeige bleibt wie sie ist */ } setBusy(false) } } @@ -149,6 +135,10 @@ export default function TicketProjects({ ticket }) { const create = (e) => { e.preventDefault() + if (!slugify(createForm.subdomain)) { + setError('Bitte eine gültige Subdomain eingeben (Buchstaben, Ziffern, Bindestrich).') + return + } run(async () => { const subdomain = slugify(createForm.subdomain) await createProjectFromTemplate({ @@ -157,6 +147,8 @@ export default function TicketProjects({ ticket }) { subdomain, customerId: ticket.customerId || '', ticketId: ticket.$id, + // Aus der Website-Vorlage mit Subdomain: die Art steht fest (sonst "Art fehlt") + kind: 'website', }) setCreateForm({ displayName: '', subdomain: '' }) setShowCreate(false) diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx index 2e39052..5387244 100644 --- a/src/components/customer/CustomerFunktionen.jsx +++ b/src/components/customer/CustomerFunktionen.jsx @@ -3,18 +3,19 @@ import { Link } from 'react-router-dom' import { FaCheck, FaCopy, FaRotateRight, FaSpinner } from 'react-icons/fa6' import { Card, Badge, Button, EmptyState } from '../ui' import { copyText } from '../CopyableCredential' -import { getFunktionen, setFeature, getTermineAnleitung } from '../../lib/teamApi' +import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi' import { FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText, - hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, + hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus, + anfrageZeilen, anfrageStatus, stripeText, stripeTon, shopHatEintrag, begruendungOk, BEGRUENDUNG_MAX, entscheidungStatus, } from '../../lib/funktionenView' import { CHANNELS, channelLabel } from '../../lib/analyseView' -import { berlinYmd } from '../../lib/format' +import { berlinYmd, fmtDateTime } from '../../lib/format' import { zeitenStatusText, mergeTeam, abschaltMeldung } from '../../lib/zeitenView' import ZeitenPanel from './ZeitenPanel' import '../../styles/admin-areas.css' -// Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") +// Reiter "Funktionen": News-Pinnwand, Terminplaner und Webshop je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") const short = (text) => (text.length > 60 ? `${text.slice(0, 60)}…` : text) // Site-Key des Terminplaners (steht im Einbau-Code der Kundenwebsite): kleine Schrift mit fester Breite und Kopierknopf @@ -115,10 +116,10 @@ function AnleitungPanel({ customerId, projectId, feature, reloadKey }) { ) } -// Zustimmung des Kunden in Textform (Terminplaner im Modus on): Haken, Datum, Weg und die Textvorlage fuer den Chat. Die Pflichtangaben +// Zustimmung des Kunden in Textform (Terminplaner und Webshop im Modus on): Haken, Datum, Weg und die Textvorlage fuer den Chat. Die Pflichtangaben // prueft textformBereit (funktionenView); das Portal prueft sie noch einmal. Links nur als geprueftes https (textformAnsicht). -function TextformPanel({ termine, value, onChange, projectId, today, disabled }) { - const ansicht = textformAnsicht(termine) +function TextformPanel({ funktion, feld, value, onChange, projectId, today, disabled }) { + const ansicht = textformAnsicht(feld, funktion) const { label, links, vorlage } = ansicht return (
+ {['deployed', 'ready'].includes(String(project.status || '').toLowerCase()) + ? <>Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert.> + : <>Preview ist unter https://{project.subdomain}.project.webklar.com eingerichtet (Status: {project.status || 'unbekannt'}). Neu bauen geht im Projekt über „Preview".>} +
+ )} + {isWebsiteWithCustomer && !hasPreviewAlready && (