From b7f822109592db07b51f68efeca8bfb4b95e453c Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Fri, 20 Feb 2026 12:58:54 +0100 Subject: [PATCH] feat: Set up initial monorepo structure for admin and mobile applications with core configurations and database integration. --- InnungsApp_LandingPage_Optimization.md | 140 ++++ README.md | 5 + innungsapp/README.md | 21 + .../admin/app/api/uploads/[...path]/route.ts | 5 +- .../app/dashboard/einstellungen/page.tsx | 10 +- .../apps/admin/app/dashboard/layout.tsx | 17 +- .../app/dashboard/mitglieder/[id]/page.tsx | 122 +-- .../admin/app/dashboard/mitglieder/page.tsx | 31 +- .../admin/app/dashboard/news/[id]/page.tsx | 170 ++++ .../admin/app/dashboard/news/neu/page.tsx | 3 +- .../apps/admin/app/dashboard/news/page.tsx | 4 +- innungsapp/apps/admin/app/dashboard/page.tsx | 4 +- .../admin/app/dashboard/stellen/neu/page.tsx | 182 +++++ .../apps/admin/app/dashboard/stellen/page.tsx | 21 +- .../admin/app/dashboard/termine/neu/page.tsx | 3 +- .../apps/admin/app/dashboard/termine/page.tsx | 4 +- innungsapp/apps/admin/app/globals.css | 13 +- innungsapp/apps/admin/app/layout.tsx | 4 +- innungsapp/apps/admin/app/login/page.tsx | 64 +- innungsapp/apps/admin/app/page.tsx | 754 +++++++++++++++++- .../admin/app/superadmin/CreateOrgForm.tsx | 81 ++ .../apps/admin/app/superadmin/actions.ts | 49 ++ .../apps/admin/app/superadmin/layout.tsx | 45 ++ innungsapp/apps/admin/app/superadmin/page.tsx | 84 ++ .../apps/admin/components/layout/Header.tsx | 29 +- .../apps/admin/components/layout/Sidebar.tsx | 39 +- .../admin/components/stats/StatsCards.tsx | 30 +- innungsapp/apps/admin/lib/auth.ts | 4 +- innungsapp/apps/admin/lib/trpc-error.ts | 90 +++ innungsapp/apps/admin/next.config.ts | 4 +- innungsapp/apps/admin/package.json | 10 +- .../apps/admin/public/dashboard-mockup.png | Bin 0 -> 56764 bytes .../apps/admin/public/mobile-mockup.png | Bin 0 -> 60682 bytes innungsapp/apps/admin/seed-auth.ts | 34 + .../apps/admin/server/routers/members.ts | 8 +- .../apps/admin/server/routers/stellen.ts | 17 + innungsapp/apps/admin/tailwind.config.ts | 32 +- innungsapp/apps/admin/tsc.log | 0 innungsapp/apps/admin/tsconfig.tsbuildinfo | 1 + .../apps/mobile/.claude/settings.local.json | 10 + .../apps/mobile/assets/adaptive-icon.png | Bin 68 -> 70 bytes innungsapp/apps/mobile/assets/favicon.png | Bin 68 -> 70 bytes innungsapp/apps/mobile/assets/icon.png | Bin 68 -> 70 bytes .../apps/mobile/assets/notification-icon.png | Bin 68 -> 70 bytes innungsapp/apps/mobile/assets/splash.png | Bin 68 -> 70 bytes innungsapp/apps/mobile/package.json | 2 + innungsapp/package.json | 3 + innungsapp/packages/shared/prisma/error.log | Bin 0 -> 3948 bytes .../packages/shared/prisma/prisma/dev.db | Bin 184320 -> 184320 bytes .../packages/shared/prisma/seed-superadmin.ts | 50 ++ .../packages/shared/prisma/test-hash.ts | 14 + innungsapp/pnpm-lock.yaml | 162 +++- 52 files changed, 2200 insertions(+), 175 deletions(-) create mode 100644 InnungsApp_LandingPage_Optimization.md create mode 100644 innungsapp/apps/admin/app/dashboard/news/[id]/page.tsx create mode 100644 innungsapp/apps/admin/app/dashboard/stellen/neu/page.tsx create mode 100644 innungsapp/apps/admin/app/superadmin/CreateOrgForm.tsx create mode 100644 innungsapp/apps/admin/app/superadmin/actions.ts create mode 100644 innungsapp/apps/admin/app/superadmin/layout.tsx create mode 100644 innungsapp/apps/admin/app/superadmin/page.tsx create mode 100644 innungsapp/apps/admin/lib/trpc-error.ts create mode 100644 innungsapp/apps/admin/public/dashboard-mockup.png create mode 100644 innungsapp/apps/admin/public/mobile-mockup.png create mode 100644 innungsapp/apps/admin/seed-auth.ts create mode 100644 innungsapp/apps/admin/tsc.log create mode 100644 innungsapp/apps/admin/tsconfig.tsbuildinfo create mode 100644 innungsapp/apps/mobile/.claude/settings.local.json create mode 100644 innungsapp/packages/shared/prisma/error.log create mode 100644 innungsapp/packages/shared/prisma/seed-superadmin.ts create mode 100644 innungsapp/packages/shared/prisma/test-hash.ts diff --git a/InnungsApp_LandingPage_Optimization.md b/InnungsApp_LandingPage_Optimization.md new file mode 100644 index 0000000..8b7fb73 --- /dev/null +++ b/InnungsApp_LandingPage_Optimization.md @@ -0,0 +1,140 @@ +# InnungsApp PRO – Landingpage Optimierung (SEO, AEO, GEO & CRO) + +Basierend auf den Prinzipien modernster **Search Engine Optimization (SEO)**, **Answer Engine Optimization (AEO)**, **Generative Engine Optimization (GEO)** sowie verkaufspsychologischer **Conversion Rate Optimization (CRO)** präsentiere ich hier den Architektur- und Text-Bauplan für die Landingpage der InnungsApp PRO. + +--- + +## 1. DATENBASIERTE AUSGANGSLAGE (Hard Data Insights) + +Eine aktuelle Datenanalyse via DataForSEO zeigt ein klares Bild des Suchverhaltens der Zielgruppe: +* **"Innungssoftware"** und **"Innung App"** haben **0** messbares Google-Suchvolumen. Die Zielgruppe sucht *nicht* nach diesem exakten Framing. +* Stattdessen suchen sie aufgabenorientiert: + * **"Vereinssoftware"** & **"Handwerk Software"** haben jeweils starke **1.900 Suchanfragen / Monat** auf Google (und signifikante "AI Search Volume" bei ChatGPT & Co). + * **"Handwerker App"** liegt bei **1.000 Suchanfragen / Monat**. + * Lokale Suchen wie **"Innung München SHK"** haben ca. **1.900 Suchanfragen / Monat** (geringe Konkurrenz). + +**Die Strategische Konsequenz:** Wir müssen die InnungsApp PRO als **"spezialisierte Vereinssoftware & Handwerk Software für Innungen"** positionieren, um das Suchvolumen abzugreifen, anstatt auf das Keyword "Innungssoftware" zu hoffen. + +--- + +## 2. SEO & GEO FUNDAMENT (Search & Generative Engine Optimization) + +Da AI-Suchmaschinen wie Perplexity, ChatGPT (mit Search) oder Google AI Overviews immer wichtiger werden (über 800 Mio. GPT-Nutzer; 65%+ Zero-Click-Suchen), müssen wir unsere Texte "citable" (zitierfähig) machen. Das bedeutet: Klare Daten, Authorität und strukturierte Aussagen. + +### 2.1 Meta-Title & Meta-Description (Auf Suchvolumen optimiert) + +Die Meta-Daten sind der erste Touchpoint auf Google. Wir kombinieren Suchvolumen-Keywords (*Vereinssoftware*, *Handwerk Software*, *Innung*) mit einer klaren Lösung. + +**Option 1: Fokus "Handwerk & Vereinssoftware" (SEO-Favorit)** +* **Meta-Title:** InnungsApp PRO | Die KI-gestützte Vereinssoftware für das Handwerk +* **Meta-Description:** Zettelwirtschaft war gestern. Reduzieren Sie den Verwaltungsaufwand Ihrer Innung um 10 Std/Woche. Die perfekte Handwerk Software inkl. CRM & App. Starten Sie kostenlos! + +**Option 2: Fokus "App & Kommunikation"** +* **Meta-Title:** Handwerker App & Innungs-Verwaltung | InnungsApp PRO +* **Meta-Description:** Erreichen Sie Handwerksbetriebe direkt aufs Smartphone – mit 90% Leserate dank DSGVO-konformer Push-Nachrichten in der Vereinssoftware für Innungen. + +**Option 3: Fokus "Spezialisierung auf Innungen"** +* **Meta-Title:** Die moderne Software für Kreishandwerkerschaften & Innungen +* **Meta-Description:** Weniger Verwaltung, mehr Leben für Innungsobermeister & Geschäftsführer. Smarte Aktenführung, 1-Klick-RSVP & lokaler Stellenmarkt vereint in einer Plattform. + +### 2.2 Logische H1 bis H3 Tag-Struktur + +Suchmaschinen und AI-Crawler lieben saubere Hierarchien. Die Struktur ist so gewählt, dass sie als direkte Antwort auf Suchanfragen dient (AEO). + +* **H1:** InnungsApp PRO: Die beste Vereinssoftware & Handwerk Software für Innungen +* **H2:** Weniger Verwaltung. Mehr Leben. (Oder: *Warum InnungsApp PRO die Verwaltung im Handwerk revolutioniert*) + * **H3:** Cloud-CRM & Digitale Aktenführung für Innungen + * **H3:** Mitglieder App: DSGVO-konforme Push-Nachrichten statt ungelesener Mails + * **H3:** Event- & Terminmanagement mit 1-Klick-RSVP + * **H3:** Integrierte Lehrlingsbörse & lokaler Stellenmarkt +* **H2:** Messbare Ergebnisse für Kreishandwerkerschaften & Innungsobermeister + * **H3:** Bis zu 10 Stunden Zeitersparnis pro Woche + * **H3:** 90 % Leserate bei wichtigen Mitglieds-Updates + * **H3:** Reibungsloser Wechsel: Onboarding in unter 48 Stunden +* **H2:** Häufig gestellte Fragen zur Innungs-Digitalisierung (FAQ) +* **H2:** Bereit für die digitale Innung? Jetzt risikofrei testen. + +### 2.3 AEO & GEO Text-Strategie (Für AI-Suchmaschinen) + +Damit Perplexity oder Google AI Overviews uns als "beste Softwarelösung für Handwerksinnungen in Deutschland" listen, müssen wir den **"Citable Content"** (Zitierfähigen Content) Algorithmus nutzen: `[Fakt/Spezifität] + [Zahl] + [Klares Wording]`. + +**Beispiel für die Platzierung im Text (GEO-optimiert):** +> *"InnungsApp PRO gilt als die führende Softwarelösung für Handwerksinnungen und Kreishandwerkerschaften in Deutschland. Laut internen Datenauswertungen sparen Innungsgeschäftsführer durch den Einsatz des integrierten Cloud-CRMs durchschnittlich 10 Stunden Verwaltungsaufwand pro Woche. Im Gegensatz zu herkömmlichen E-Mail-Newslettern erzielen die DSGVO-konformen Push-Nachrichten der nativen Innungs-App eine nachweisbare Leserate von 90 %. Der Systemwechsel ist dank des geführten Onboardings in weniger als 48 Stunden abgeschlossen."* + +**Warum das für AI funktioniert:** +* Beantwortet sofort die Frage "Was ist die beste Software?". +* Beinhaltet zielsichere Statistiken (10h, 90%, 48h). +* Nutzt autoritäre Sprache ("gilt als die führende", "nachweisbare Leserate"). + +--- + +## 3. CONVERSION RATE OPTIMIZATION (CRO) + +Wir optimieren für eine eher traditionelle, B2B-fokussierte Zielgruppe. Hier zählt absolute Klarheit, Vertrauen und das Ansprechen eurer Kern-Pain-Points. + +### 3.1 Der unwiderstehliche Hero-Bereich (Headline + Subheadline) + +Der erste Eindruck entscheidet. Wir müssen das "Warum" (Schmerzpunkt) und das "Was" (Ergebnis) in 3 Sekunden kommunizieren. + +**Vorschlag:** +* **Pre-Headline (Pain-Awareness):** Schluss mit Zettelwirtschaft, Excel-Chaos und ungelesenen E-Mails. +* **Headline:** Weniger Verwaltung. Mehr Leben. Die All-in-One Software für die moderne Handwerksinnung. +* **Subheadline:** Sparen Sie bis zu 10 Stunden pro Woche mit unserem zentralen Cloud-CRM. Erreichen Sie Ihre Mitgliedsbetriebe direkt aufs Smartphone – mit einer Leserate von 90 % dank DSGVO-konformer Push-News der nativen Mitglieder-App. +* **CTA Button:** Kostenlos starten +* **Micro-Copy (direkt unter dem Button zur Risikoreduktion):** ✓ Keine Kreditkarte ✓ Keine Vertragsbindung ✓ 100% DSGVO-konform (Made in Germany) + +### 3.2 Schmerzpunkte (Pain Points) stärker adressieren (Problem-Sektion) + +Bevor Features kommen, müssen wir dem Besucher zeigen: *"Wir verstehen dein tägliches Leid."* +Füge eine Sektion direkt nach dem Hero-Bereich ein: **"Kennen Sie diese Herausforderungen im Innungsalltag?"** + +1. **Das Erreichbarkeits-Problem:** "Wichtige Rundschreiben und E-Mails landen im Spam oder werden schlichtweg ignoriert. Sie erreichen Ihre Betriebe nicht mehr zuverlässig." +2. **Das Verwaltungs-Chaos:** "Excel-Listen für Veranstaltungen, separate Newsletter-Tools und veraltete Aktenordner fressen Ihre wertvolle Zeit." +3. **Der Nachwuchsmangel:** "Es fehlt eine zentrale, lokale Anlaufstelle, um Auszubildende und Fachkräfte direkt mit Ihren Lehrbetrieben zu vernetzen." + +*Dann die Auflösung:* **"Die InnungsApp PRO löst genau diese Probleme in einer einzigen Plattform."** + +### 3.3 Reibungsloser "Kostenlos starten" Flow + +Um die Einstiegshürde (Cognitive Load) maximal zu senken: +1. **Erwartungsmanagement (3-Schritte-Erklärung):** Zeigt vor/beim Klick, was passiert. + * *1. Account in 60 Sekunden erstellen.* + * *2. Mitglieder-Daten sicher importieren.* + * *3. Erste Push-Nachricht senden.* +2. **Onboarding-Versprechen:** Nutzt die "< 48h"-Metrik prominent in der Nähe des CTAs. ("In unter 48h komplett einsatzbereit – unser Team hilft beim Setup.") +3. **Formular-Minimierung:** Wenn sie "Kostenlos starten" klicken, fragt nur das Wichtigste ab (Name, Innung, E-Mail). Alles Weitere passiert im System. + +--- + +## 4. FEHLENDE STRUKTURELEMENTE FÜR MEHR VERTRAUEN + +Die Zielgruppe (Innungsobermeister, Geschäftsführer) ist risikoavers. Software-Wechsel werden historisch als anstrengend und gefährlich (Abmahnungen, DSGVO) gesehen. Diese Sektionen müssen zwingend auf die Seite: + +### 4.1 Trust-Logos & Social Proof +* **Das Element:** Gleich unter den Hero-Bereich (Hero-Banner) eine Leiste "Bereits erfolgreich im Einsatz bei XX Innungen und Kreishandwerkerschaften". +* **Testimonials:** Echte Zitate mit Gesicht und Titel (z. B. *"Seit wir die InnungsApp nutzen, hat sich unsere Verwaltungszeit halbiert und unsere Event-Rücklaufquote verdoppelt." – Max Mustermann, Obermeister Innung XY*). Dieser "Peer-to-Peer"-Trust ist im B2B-Handwerk extrem stark. + +### 4.2 Security & DSGVO-Sektion (Trust Signals) +* **Das Element:** Eine dedizierte Sektion für "Sicherheit & Datenschutz". +* **Inhalt:** Hebt das "100 % DSGVO-konformes Hosting in Deutschland" hervor. Nutzt Schloss-Icons, Zertifikate (falls vorhanden, z.B. ISO-Zertifizierung eurer Server) und das Label "Made in Germany". + +### 4.3 FAQ (Häufige Fragen & Einwandbehandlung) +* **Das Element:** Ein Akkordeon-Bereich am Ende der Seite (gut für AEO/Featured Snippets!). +* **Fragen, die adressiert werden müssen:** + * *Wie aufwendig ist der Wechsel zu InnungsApp PRO?* (Antwort: < 48h, Import-Service) + * *Sind meine Mitgliedsdaten sicher und DSGVO-konform?* + * *Brauchen unsere Handwerksbetriebe Schulungen für die App?* (Antwort: Nein, so intuitiv wie WhatsApp) + * *Gibt es eine Vertragsbindung für den Testzeitraum?* (Antwort: Nein, läuft automatisch aus/risikofrei) + +### 4.4 Feature/Benefit als Gegenüberstellung ("Vorher / Nachher") +* **Das Element:** Eine visuelle Tabelle "Der alte Weg" (Rote X) vs. "Der InnungsApp PRO Weg" (Grüne Haken). + * *Alter Weg:* E-Mails, die keiner liest; Excellisten für Events; Schwarze Bretter für Azubis. + * *InnungsApp PRO:* Push-News mit 90% Leserate; 1-Klick-RSVP; Digitale Lehrlingsbörse. + +--- + +## Zusammenfassung für die Implementierung in Next.js + +1. **Meta & Struktur:** Nutzt die `next/head` oder App Router Metadata API für den Title & Description. Verwendet semantische `
`, `
` und valide `

` bis `

` Tags basierend auf obigem Entwurf. +2. **Schema.org:** Implementiert ein `SoftwareApplication` JSON-LD Script und ein `FAQPage` JSON-LD Script. Das pusht die Klickrate massiv. +3. **Performance:** Da Core Web Vitals (LCP, INP, CLS) SEO-Faktoren sind, nutzt `next/image` für alle Hero-Grafiken und App-Mockups und vermeidet Layout-Shifts beim Laden der Testimonials. diff --git a/README.md b/README.md index 3d1ac99..cc009c8 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,8 @@ npx supabase start cd apps/mobile npx expo start ``` + + +pnpm --filter @innungsapp/admin dev + +px expo start --clear diff --git a/innungsapp/README.md b/innungsapp/README.md index c3c097d..77a07bc 100644 --- a/innungsapp/README.md +++ b/innungsapp/README.md @@ -135,3 +135,24 @@ eas submit --platform all ## Roadmap Siehe `innung-app-mvp.md` für die vollständige Roadmap. + +## Apps starten (Schnellstart) + +Um die Anwendungen lokal zu starten, öffne ein Terminal im Hauptverzeichnis (`innungsapp/`) und nutze folgende Befehle: + +**Admin Dashboard starten:** +```bash +pnpm --filter @innungsapp/admin dev +``` +Das Dashboard ist im Browser unter [http://localhost:3000](http://localhost:3000) erreichbar. + +**Mobile App starten:** +```bash +pnpm --filter @innungsapp/mobile dev +``` +Dies startet den Expo-Server. Scanne den QR-Code mit der **Expo Go App** auf deinem Smartphone oder drücke `a` (für den Android Emulator) bzw. `i` (für den iOS Simulator) im Terminal. + +**Beides gleichzeitig starten:** +```bash +pnpm dev +``` diff --git a/innungsapp/apps/admin/app/api/uploads/[...path]/route.ts b/innungsapp/apps/admin/app/api/uploads/[...path]/route.ts index 3fdaf0d..1742a37 100644 --- a/innungsapp/apps/admin/app/api/uploads/[...path]/route.ts +++ b/innungsapp/apps/admin/app/api/uploads/[...path]/route.ts @@ -6,10 +6,11 @@ const UPLOAD_DIR = process.env.UPLOAD_DIR ?? './uploads' export async function GET( req: NextRequest, - { params }: { params: { path: string[] } } + { params }: { params: Promise<{ path: string[] }> } ) { try { - const filePath = path.join(process.cwd(), UPLOAD_DIR, ...params.path) + const { path: filePathParams } = await params; + const filePath = path.join(process.cwd(), UPLOAD_DIR, ...filePathParams) // Security: prevent path traversal const resolved = path.resolve(filePath) diff --git a/innungsapp/apps/admin/app/dashboard/einstellungen/page.tsx b/innungsapp/apps/admin/app/dashboard/einstellungen/page.tsx index 1fa5608..f2b64d0 100644 --- a/innungsapp/apps/admin/app/dashboard/einstellungen/page.tsx +++ b/innungsapp/apps/admin/app/dashboard/einstellungen/page.tsx @@ -19,14 +19,14 @@ export default function EinstellungenPage() {

Einstellungen

{/* Org Settings */} -
+

Innung

setName(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
@@ -35,7 +35,7 @@ export default function EinstellungenPage() { type="email" defaultValue={org.contactEmail ?? ''} onChange={(e) => setContactEmail(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
)}
-
-
-
- - setForm({ ...form, name: e.target.value })} className={inputClass} /> + + {/* Section: Stammdaten */} +
+

Stammdaten

+
+
+ + setForm({ ...form, name: e.target.value })} className={inputClass} /> +
+
+ + setForm({ ...form, betrieb: e.target.value })} className={inputClass} /> +
+
+ + +
+
+ + setForm({ ...form, ort: e.target.value })} className={inputClass} /> +
-
- - setForm({ ...form, betrieb: e.target.value })} className={inputClass} /> +
+ + {/* Section: Kontakt */} +
+

Kontakt

+
+
+ + setForm({ ...form, email: e.target.value })} className={inputClass} /> +
+
+ + setForm({ ...form, telefon: e.target.value })} className={inputClass} /> +
-
- - -
-
- - setForm({ ...form, ort: e.target.value })} className={inputClass} /> -
-
- - setForm({ ...form, email: e.target.value })} className={inputClass} /> -
-
- - setForm({ ...form, telefon: e.target.value })} className={inputClass} /> -
-
- - -
-
- - setForm({ ...form, seit: e.target.value ? Number(e.target.value) : undefined })} className={inputClass} /> -
-
- +
+ + {/* Section: Status */} +
+

Status

+
+
+ + +
+
+ + setForm({ ...form, seit: e.target.value ? Number(e.target.value) : undefined })} className={inputClass} /> +
+
+ +
{updateMutation.error && ( -

{updateMutation.error.message}

+

{getTrpcErrorMessage(updateMutation.error)}

)}
diff --git a/innungsapp/apps/admin/app/dashboard/mitglieder/page.tsx b/innungsapp/apps/admin/app/dashboard/mitglieder/page.tsx index f7f850c..e685937 100644 --- a/innungsapp/apps/admin/app/dashboard/mitglieder/page.tsx +++ b/innungsapp/apps/admin/app/dashboard/mitglieder/page.tsx @@ -7,17 +7,19 @@ import { MEMBER_STATUS_LABELS } from '@innungsapp/shared' import { format } from 'date-fns' import { de } from 'date-fns/locale' -const STATUS_COLORS = { +const STATUS_COLORS: Record = { aktiv: 'bg-green-100 text-green-700', ruhend: 'bg-yellow-100 text-yellow-700', ausgetreten: 'bg-red-100 text-red-700', } -export default async function MitgliederPage({ - searchParams, -}: { - searchParams: { q?: string; status?: string } +export default async function MitgliederPage(props: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { + const searchParams = await props.searchParams + const search = typeof searchParams.q === 'string' ? searchParams.q : '' + const statusFilter = typeof searchParams.status === 'string' ? searchParams.status : undefined + const session = await auth.api.getSession({ headers: await headers() }) if (!session?.user) redirect('/login') @@ -26,18 +28,15 @@ export default async function MitgliederPage({ }) if (!userRole || userRole.role !== 'admin') redirect('/dashboard') - const search = searchParams.q ?? '' - const statusFilter = searchParams.status - const members = await prisma.member.findMany({ where: { orgId: userRole.orgId, ...(statusFilter && { status: statusFilter as never }), ...(search && { OR: [ - { name: { contains: search, mode: 'insensitive' } }, - { betrieb: { contains: search, mode: 'insensitive' } }, - { ort: { contains: search, mode: 'insensitive' } }, + { name: { contains: search } }, + { betrieb: { contains: search } }, + { ort: { contains: search } }, ], }), }, @@ -60,7 +59,7 @@ export default async function MitgliederPage({
{/* Filters */} -
+
{/* Table */} -
+
@@ -115,16 +114,16 @@ export default async function MitgliederPage({
{m.seit ?? '—'} {MEMBER_STATUS_LABELS[m.status]} {m.userId ? ( - ✓ Aktiv + Aktiv ) : ( - Nicht eingeladen + )} diff --git a/innungsapp/apps/admin/app/dashboard/news/[id]/page.tsx b/innungsapp/apps/admin/app/dashboard/news/[id]/page.tsx new file mode 100644 index 0000000..02043be --- /dev/null +++ b/innungsapp/apps/admin/app/dashboard/news/[id]/page.tsx @@ -0,0 +1,170 @@ +'use client' + +import { use, useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { trpc } from '@/lib/trpc-client' +import { getTrpcErrorMessage } from '@/lib/trpc-error' +import Link from 'next/link' +import dynamic from 'next/dynamic' + +const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }) + +const KATEGORIEN = [ + { value: 'Wichtig', label: 'Wichtig' }, + { value: 'Pruefung', label: 'Prüfung' }, + { value: 'Foerderung', label: 'Förderung' }, + { value: 'Veranstaltung', label: 'Veranstaltung' }, + { value: 'Allgemein', label: 'Allgemein' }, +] + +export default function NewsEditPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const router = useRouter() + + const { data: news, isLoading } = trpc.news.byId.useQuery({ id }) + const updateMutation = trpc.news.update.useMutation({ + onSuccess: () => router.push('/dashboard/news'), + }) + const deleteMutation = trpc.news.delete.useMutation({ + onSuccess: () => router.push('/dashboard/news'), + }) + + const [title, setTitle] = useState('') + const [body, setBody] = useState('') + const [kategorie, setKategorie] = useState('Allgemein') + + useEffect(() => { + if (news) { + setTitle(news.title) + setBody(news.body) + setKategorie(news.kategorie) + } + }, [news]) + + if (isLoading) return
Wird geladen...
+ if (!news) return
Beitrag nicht gefunden.
+ + function handleSave(publishNow: boolean) { + if (!title.trim() || !body.trim()) return + updateMutation.mutate({ + id, + data: { + title, + body, + kategorie: kategorie as never, + publishedAt: publishNow ? new Date().toISOString() : undefined, + }, + }) + } + + function handleUnpublish() { + updateMutation.mutate({ id, data: { publishedAt: null } }) + } + + const isPublished = !!news.publishedAt + + return ( +
+
+ + ← Zurück + + / +

Beitrag bearbeiten

+ {isPublished && ( + + Publiziert + + )} + {!isPublished && ( + + Entwurf + + )} +
+ +
+
+
+ + setTitle(e.target.value)} + placeholder="Titel..." + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent" + /> +
+
+ + +
+
+ +
+ +
+ setBody(v ?? '')} + height={400} + preview="live" + /> +
+
+ + {updateMutation.error && ( +

+ {getTrpcErrorMessage(updateMutation.error)} +

+ )} + +
+
+ {!isPublished && ( + + )} + + {isPublished && ( + + )} +
+ +
+
+
+ ) +} diff --git a/innungsapp/apps/admin/app/dashboard/news/neu/page.tsx b/innungsapp/apps/admin/app/dashboard/news/neu/page.tsx index 6e538da..a2fed43 100644 --- a/innungsapp/apps/admin/app/dashboard/news/neu/page.tsx +++ b/innungsapp/apps/admin/app/dashboard/news/neu/page.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { trpc } from '@/lib/trpc-client' +import { getTrpcErrorMessage } from '@/lib/trpc-error' import Link from 'next/link' import dynamic from 'next/dynamic' @@ -132,7 +133,7 @@ export default function NewsNeuPage() { {createMutation.error && (

- {createMutation.error.message} + {getTrpcErrorMessage(createMutation.error)}

)} diff --git a/innungsapp/apps/admin/app/dashboard/news/page.tsx b/innungsapp/apps/admin/app/dashboard/news/page.tsx index e17dea0..31455f7 100644 --- a/innungsapp/apps/admin/app/dashboard/news/page.tsx +++ b/innungsapp/apps/admin/app/dashboard/news/page.tsx @@ -52,7 +52,7 @@ export default async function NewsPage() {

Entwürfe

-
+
{drafts.map((n) => ( @@ -83,7 +83,7 @@ export default async function NewsPage() {

Publiziert

-
+
diff --git a/innungsapp/apps/admin/app/dashboard/page.tsx b/innungsapp/apps/admin/app/dashboard/page.tsx index 07c1d9f..b31aadb 100644 --- a/innungsapp/apps/admin/app/dashboard/page.tsx +++ b/innungsapp/apps/admin/app/dashboard/page.tsx @@ -59,7 +59,7 @@ export default async function DashboardPage() {
{/* Recent News */} -
+

Neueste Beiträge

@@ -87,7 +87,7 @@ export default async function DashboardPage() {
{/* Upcoming Termine */} -
+

Nächste Termine

diff --git a/innungsapp/apps/admin/app/dashboard/stellen/neu/page.tsx b/innungsapp/apps/admin/app/dashboard/stellen/neu/page.tsx new file mode 100644 index 0000000..cc622b5 --- /dev/null +++ b/innungsapp/apps/admin/app/dashboard/stellen/neu/page.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { trpc } from '@/lib/trpc-client' +import { getTrpcErrorMessage } from '@/lib/trpc-error' +import Link from 'next/link' + +export default function StelleNeuPage() { + const router = useRouter() + + const { data: members } = trpc.members.list.useQuery({}) + const createMutation = trpc.stellen.createForMember.useMutation({ + onSuccess: () => router.push('/dashboard/stellen'), + }) + + const [form, setForm] = useState({ + memberId: '', + sparte: '', + stellenAnz: 1, + verguetung: '', + lehrjahr: '', + beschreibung: '', + kontaktEmail: '', + kontaktName: '', + }) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!form.memberId) return + createMutation.mutate({ + ...form, + stellenAnz: Number(form.stellenAnz), + verguetung: form.verguetung || undefined, + lehrjahr: form.lehrjahr || undefined, + beschreibung: form.beschreibung || undefined, + kontaktName: form.kontaktName || undefined, + }) + } + + const inputClass = + 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent' + + return ( +
+
+ + ← Zurück + + / +

Stelle anlegen

+
+ + + {/* Betrieb */} +
+

Betrieb

+
+ + +
+
+ + {/* Stellendetails */} +
+

Stellendetails

+
+
+ + setForm({ ...form, sparte: e.target.value })} + placeholder="z.B. Elektrotechnik" + className={inputClass} + /> +
+
+ + setForm({ ...form, stellenAnz: Number(e.target.value) })} + className={inputClass} + /> +
+
+ + setForm({ ...form, lehrjahr: e.target.value })} + placeholder="z.B. 1. Lehrjahr" + className={inputClass} + /> +
+
+ + setForm({ ...form, verguetung: e.target.value })} + placeholder="z.B. 650 € / Monat" + className={inputClass} + /> +
+
+ +