feat: Implement initial with admin and mobile clients, authentication, data models, and lead generation scripts.

This commit is contained in:
Timo Knuth 2026-02-19 16:18:34 +01:00
parent c53a71a5f9
commit 5e2d5fb3ae
32 changed files with 2283 additions and 420 deletions

View File

@ -1,7 +1,9 @@
# ============================================= # =============================================
# DATABASE # DATABASE (SQLite — kein externer DB-Server nötig)
# Dev: file:../../packages/shared/prisma/dev.db
# Prod: file:./prisma/prod.db
# ============================================= # =============================================
DATABASE_URL="postgresql://postgres:password@localhost:5432/innungsapp" DATABASE_URL="file:../../packages/shared/prisma/dev.db"
# ============================================= # =============================================
# BETTER-AUTH # BETTER-AUTH

View File

@ -0,0 +1,60 @@
/**
* DEV-ONLY: Sets a password for the demo admin user via better-auth.
* Call once after seeding: GET http://localhost:3032/api/setup
* Remove this file before going to production.
*/
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@innungsapp/shared'
export async function GET() {
if (process.env.NODE_ENV === 'production') {
return NextResponse.json({ error: 'Not available in production' }, { status: 403 })
}
// Delete the pre-seeded user so better-auth can create it fresh with a hashed password
await prisma.account.deleteMany({ where: { userId: 'demo-admin-user-id' } })
await prisma.member.deleteMany({ where: { userId: 'demo-admin-user-id' } })
await prisma.userRole.deleteMany({ where: { userId: 'demo-admin-user-id' } })
await prisma.user.deleteMany({ where: { id: 'demo-admin-user-id' } })
// Re-create via better-auth so the password is properly hashed
const result = await auth.api.signUpEmail({
body: { email: 'admin@demo.de', password: 'demo1234', name: 'Demo Admin' },
})
if (!result?.user) {
return NextResponse.json({ error: 'signUp failed', result }, { status: 500 })
}
const newUserId = result.user.id
// Restore org membership for the new user ID
const org = await prisma.organization.findFirst({ where: { slug: 'innung-elektro-stuttgart' } })
if (org) {
await prisma.userRole.upsert({
where: { orgId_userId: { orgId: org.id, userId: newUserId } },
update: {},
create: { orgId: org.id, userId: newUserId, role: 'admin' },
})
await prisma.member.upsert({
where: { userId: newUserId },
update: {},
create: {
orgId: org.id,
userId: newUserId,
name: 'Demo Admin',
betrieb: 'Innungsgeschäftsstelle',
sparte: 'Elektrotechnik',
ort: 'Stuttgart',
email: 'admin@demo.de',
status: 'aktiv',
},
})
}
return NextResponse.json({
ok: true,
message: 'Setup complete. Login: admin@demo.de / demo1234',
})
}

View File

@ -3,14 +3,16 @@
import { useState } from 'react' import { useState } from 'react'
import { createAuthClient } from 'better-auth/react' import { createAuthClient } from 'better-auth/react'
import { magicLinkClient } from 'better-auth/client/plugins' import { magicLinkClient } from 'better-auth/client/plugins'
const authClient = createAuthClient({ const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
plugins: [magicLinkClient()], plugins: [magicLinkClient()],
}) })
type Mode = 'password' | 'magic'
export default function LoginPage() { export default function LoginPage() {
const [mode, setMode] = useState<Mode>('password')
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [sent, setSent] = useState(false) const [sent, setSent] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@ -20,11 +22,23 @@ export default function LoginPage() {
setLoading(true) setLoading(true)
setError('') setError('')
if (mode === 'password') {
const result = await authClient.signIn.email({
email,
password,
callbackURL: '/dashboard',
})
setLoading(false)
if (result.error) {
setError(result.error.message ?? 'E-Mail oder Passwort falsch.')
} else {
window.location.href = '/dashboard'
}
} else {
const result = await authClient.signIn.magicLink({ const result = await authClient.signIn.magicLink({
email, email,
callbackURL: '/dashboard', callbackURL: '/dashboard',
}) })
setLoading(false) setLoading(false)
if (result.error) { if (result.error) {
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.') setError(result.error.message ?? 'Ein Fehler ist aufgetreten.')
@ -32,6 +46,7 @@ export default function LoginPage() {
setSent(true) setSent(true)
} }
} }
}
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
@ -53,9 +68,7 @@ export default function LoginPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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 strokeLinecap="round" strokeLinejoin="round" strokeWidth={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> </svg>
</div> </div>
<h2 className="text-xl font-semibold text-gray-900 mb-2"> <h2 className="text-xl font-semibold text-gray-900 mb-2">E-Mail gesendet!</h2>
E-Mail gesendet!
</h2>
<p className="text-gray-500"> <p className="text-gray-500">
Wir haben einen Login-Link an <strong>{email}</strong> gesendet. Wir haben einen Login-Link an <strong>{email}</strong> gesendet.
Bitte überprüfen Sie Ihr Postfach. Bitte überprüfen Sie Ihr Postfach.
@ -69,15 +82,33 @@ export default function LoginPage() {
</div> </div>
) : ( ) : (
<> <>
<h2 className="text-xl font-semibold text-gray-900 mb-6"> <h2 className="text-xl font-semibold text-gray-900 mb-6">Anmelden</h2>
Anmelden
</h2> {/* Mode toggle */}
<div className="flex rounded-lg border border-gray-200 p-1 mb-6">
<button
type="button"
onClick={() => setMode('password')}
className={`flex-1 py-1.5 text-sm rounded-md font-medium transition-colors ${
mode === 'password' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-gray-700'
}`}
>
Passwort
</button>
<button
type="button"
onClick={() => setMode('magic')}
className={`flex-1 py-1.5 text-sm rounded-md font-medium transition-colors ${
mode === 'magic' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-gray-700'
}`}
>
Magic Link
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
E-Mail-Adresse E-Mail-Adresse
</label> </label>
<input <input
@ -91,10 +122,25 @@ export default function LoginPage() {
/> />
</div> </div>
{mode === 'password' && (
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Passwort
</label>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
)}
{error && ( {error && (
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg"> <p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">{error}</p>
{error}
</p>
)} )}
<button <button
@ -102,13 +148,19 @@ export default function LoginPage() {
disabled={loading} disabled={loading}
className="w-full bg-brand-500 text-white py-2.5 px-4 rounded-lg font-medium hover:bg-brand-600 disabled:opacity-60 disabled:cursor-not-allowed transition-colors" className="w-full bg-brand-500 text-white py-2.5 px-4 rounded-lg font-medium hover:bg-brand-600 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
> >
{loading ? 'Wird gesendet...' : 'Magic Link senden'} {loading
? 'Bitte warten...'
: mode === 'password'
? 'Anmelden'
: 'Magic Link senden'}
</button> </button>
</form> </form>
<p className="mt-4 text-center text-sm text-gray-500"> {mode === 'password' && (
Kein Passwort nötig Sie erhalten einen Link per E-Mail. <p className="mt-4 text-center text-xs text-gray-400">
Demo: admin@demo.de / demo1234
</p> </p>
)}
</> </>
)} )}
</div> </div>

View File

@ -7,13 +7,19 @@ import { sendMagicLinkEmail } from './email'
export const auth = betterAuth({ export const auth = betterAuth({
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {
provider: 'postgresql', provider: 'sqlite',
}), }),
emailAndPassword: {
enabled: true,
},
secret: process.env.BETTER_AUTH_SECRET!, secret: process.env.BETTER_AUTH_SECRET!,
baseURL: process.env.BETTER_AUTH_URL!, baseURL: process.env.BETTER_AUTH_URL!,
trustedOrigins: [ trustedOrigins: [
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000', process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3032',
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000', process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3032',
'http://10.36.148.233:3032',
'http://localhost:8081', // Expo dev client
'http://10.36.148.233:8081',
], ],
plugins: [ plugins: [
magicLink({ magicLink({

View File

@ -1,7 +1,7 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
const PUBLIC_PATHS = ['/login', '/api/auth', '/api/trpc/stellen.listPublic'] const PUBLIC_PATHS = ['/login', '/api/auth', '/api/trpc/stellen.listPublic', '/api/setup']
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname const pathname = request.nextUrl.pathname

5
innungsapp/apps/admin/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -19,7 +19,7 @@
"@trpc/server": "^11.0.0", "@trpc/server": "^11.0.0",
"@tanstack/react-query": "^5.59.0", "@tanstack/react-query": "^5.59.0",
"better-auth": "^1.2.0", "better-auth": "^1.2.0",
"next": "^15.0.0", "next": "15.3.4",
"react": "^18.3.0", "react": "^18.3.0",
"react-dom": "^18.3.0", "react-dom": "^18.3.0",
"zod": "^3.23.0", "zod": "^3.23.0",

View File

@ -124,6 +124,13 @@ export const membersRouter = router({
where: { id: input.id, orgId: ctx.orgId }, where: { id: input.id, orgId: ctx.orgId },
data: input.data, data: input.data,
}) })
// Keep user.name in sync when member name changes
if (input.data.name) {
const m = await ctx.prisma.member.findFirst({ where: { id: input.id }, select: { userId: true } })
if (m?.userId) {
await ctx.prisma.user.update({ where: { id: m.userId }, data: { name: input.data.name } })
}
}
return member return member
}), }),

View File

@ -8,6 +8,7 @@ import { NEWS_KATEGORIE_LABELS } from '@innungsapp/shared/types'
import { useNewsList } from '@/hooks/useNews' import { useNewsList } from '@/hooks/useNews'
import { useTermineListe } from '@/hooks/useTermine' import { useTermineListe } from '@/hooks/useTermine'
import { useNewsReadStore } from '@/store/news.store' import { useNewsReadStore } from '@/store/news.store'
import { trpc } from '@/lib/trpc'
// Helper to truncate text // Helper to truncate text
function getNewsExcerpt(value: string) { function getNewsExcerpt(value: string) {
@ -23,16 +24,18 @@ export default function HomeScreen() {
const { data: newsItems = [] } = useNewsList() const { data: newsItems = [] } = useNewsList()
const { data: termine = [] } = useTermineListe(true) const { data: termine = [] } = useTermineListe(true)
const readIds = useNewsReadStore((s) => s.readIds) const readIds = useNewsReadStore((s) => s.readIds)
const { data: me } = trpc.members.me.useQuery()
const userName = me?.name ?? ''
const latestNews = newsItems.slice(0, 2) const latestNews = newsItems.slice(0, 2)
const upcomingEvents = termine.slice(0, 3) const upcomingEvents = termine.slice(0, 3)
const unreadCount = newsItems.filter((item) => !(item.isRead || readIds.has(item.id))).length const unreadCount = newsItems.filter((item) => !(item.isRead || readIds.has(item.id))).length
const QUICK_ACTIONS = [ const QUICK_ACTIONS = [
{ label: 'Mitglieder', icon: 'people', color: '#003B7E', bg: '#E0F2FE', route: '/(app)/members' }, { label: 'Mitglieder', icon: 'people-circle', color: '#2563EB', bg: '#DBEAFE', route: '/(app)/members' },
{ label: 'Termine', icon: 'calendar', color: '#B45309', bg: '#FEF3C7', route: '/(app)/termine' }, { label: 'Termine', icon: 'alarm', color: '#EA580C', bg: '#FFEDD5', route: '/(app)/termine' },
{ label: 'Stellen', icon: 'briefcase', color: '#059669', bg: '#D1FAE5', route: '/(app)/stellen' }, { label: 'Stellen', icon: 'construct', color: '#0F766E', bg: '#CCFBF1', route: '/(app)/stellen' },
{ label: 'Profil', icon: 'person', color: '#4F46E5', bg: '#E0E7FF', route: '/(app)/profil' }, { label: 'Aktuelles', icon: 'megaphone', color: '#BE185D', bg: '#FCE7F3', route: '/(app)/news' },
] ]
return ( return (
@ -53,7 +56,7 @@ export default function HomeScreen() {
</View> </View>
<View> <View>
<Text style={styles.greeting}>Willkommen zurück,</Text> <Text style={styles.greeting}>Willkommen zurück,</Text>
<Text style={styles.username}>Demo Admin</Text> <Text style={styles.username}>{userName}</Text>
</View> </View>
</View> </View>
@ -461,4 +464,3 @@ const styles = StyleSheet.create({
fontWeight: '500', fontWeight: '500',
}, },
}) })

View File

@ -2,7 +2,7 @@ import { View, Text, ScrollView, TouchableOpacity, Alert, StyleSheet } from 'rea
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { Ionicons } from '@expo/vector-icons' import { Ionicons } from '@expo/vector-icons'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { MOCK_MEMBER_ME } from '@/lib/mock-data' import { trpc } from '@/lib/trpc'
type Item = { type Item = {
label: string label: string
@ -20,9 +20,10 @@ const MENU_ITEMS: Item[] = [
export default function ProfilScreen() { export default function ProfilScreen() {
const { signOut } = useAuth() const { signOut } = useAuth()
const member = MOCK_MEMBER_ME const { data: me } = trpc.members.me.useQuery()
const name = me?.name ?? ''
const initials = member.name const initials = name
.split(' ') .split(' ')
.slice(0, 2) .slice(0, 2)
.map((chunk) => chunk[0]?.toUpperCase() ?? '') .map((chunk) => chunk[0]?.toUpperCase() ?? '')
@ -42,7 +43,7 @@ export default function ProfilScreen() {
<Ionicons name="settings-outline" size={15} color="#64748B" /> <Ionicons name="settings-outline" size={15} color="#64748B" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={styles.name}>{member.name}</Text> <Text style={styles.name}>{name}</Text>
<Text style={styles.role}>Innungsgeschaeftsfuehrer</Text> <Text style={styles.role}>Innungsgeschaeftsfuehrer</Text>
<View style={styles.badgesRow}> <View style={styles.badgesRow}>
<View style={styles.statusBadge}> <View style={styles.statusBadge}>

View File

@ -7,29 +7,40 @@ import { useRouter } from 'expo-router'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { Ionicons } from '@expo/vector-icons' import { Ionicons } from '@expo/vector-icons'
import { authClient } from '@/lib/auth-client' import { authClient } from '@/lib/auth-client'
import { useAuthStore } from '@/store/auth.store'
export default function LoginScreen() { export default function LoginScreen() {
const router = useRouter() const router = useRouter()
const setSession = useAuthStore((s) => s.setSession)
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const canSubmit = email.trim().length > 0 && !loading const canSubmit = email.trim().length > 0 && password.length > 0 && !loading
async function handleSendLink() { async function handleLogin() {
if (!email.trim()) return if (!canSubmit) return
setLoading(true) setLoading(true)
setError('') setError('')
const result = await authClient.signIn.magicLink({
const result = await authClient.signIn.email({
email: email.trim().toLowerCase(), email: email.trim().toLowerCase(),
callbackURL: '/home', password,
}) })
setLoading(false)
if (result.error) { if (result.error) {
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.') setError(result.error.message ?? 'E-Mail oder Passwort falsch.')
} else { setLoading(false)
router.push({ pathname: '/(auth)/check-email', params: { email } }) return
} }
const token = (result.data as any)?.session?.token
const user = (result.data as any)?.user
await setSession(user ? { user } : null, token)
setLoading(false)
router.replace('/(app)/home' as never)
} }
return ( return (
@ -60,7 +71,21 @@ export default function LoginScreen() {
autoCorrect={false} autoCorrect={false}
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
onSubmitEditing={handleSendLink} returnKeyType="next"
/>
</View>
<Text style={[styles.inputLabel, { marginTop: 4 }]}>Passwort</Text>
<View style={styles.inputWrap}>
<Ionicons name="lock-closed-outline" size={18} color="#94A3B8" />
<TextInput
style={styles.input}
placeholder="••••••••"
placeholderTextColor="#94A3B8"
secureTextEntry
value={password}
onChangeText={setPassword}
onSubmitEditing={handleLogin}
returnKeyType="go" returnKeyType="go"
/> />
</View> </View>
@ -72,7 +97,7 @@ export default function LoginScreen() {
) : null} ) : null}
<TouchableOpacity <TouchableOpacity
onPress={handleSendLink} onPress={handleLogin}
disabled={!canSubmit} disabled={!canSubmit}
style={[styles.submitBtn, !canSubmit && styles.submitBtnDisabled]} style={[styles.submitBtn, !canSubmit && styles.submitBtnDisabled]}
activeOpacity={0.85} activeOpacity={0.85}
@ -81,7 +106,7 @@ export default function LoginScreen() {
<ActivityIndicator color="#FFFFFF" /> <ActivityIndicator color="#FFFFFF" />
) : ( ) : (
<View style={styles.submitContent}> <View style={styles.submitContent}>
<Text style={styles.submitLabel}>Login-Link senden</Text> <Text style={styles.submitLabel}>Anmelden</Text>
<Ionicons name="arrow-forward" size={16} color="#FFFFFF" /> <Ionicons name="arrow-forward" size={16} color="#FFFFFF" />
</View> </View>
)} )}
@ -89,7 +114,7 @@ export default function LoginScreen() {
</View> </View>
<Text style={styles.hint}> <Text style={styles.hint}>
Noch kein Zugang? Kontaktieren Sie Ihre Innungsgeschaeftsstelle. Noch kein Zugang? Kontaktieren Sie Ihre Innungsgeschäftsstelle.
</Text> </Text>
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
@ -98,109 +123,38 @@ export default function LoginScreen() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeArea: { safeArea: { flex: 1, backgroundColor: '#FFFFFF' },
flex: 1, keyboardView: { flex: 1 },
backgroundColor: '#FFFFFF', content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
}, logoSection: { alignItems: 'center', marginBottom: 40 },
keyboardView: {
flex: 1,
},
content: {
flex: 1,
justifyContent: 'center',
paddingHorizontal: 24,
},
logoSection: {
alignItems: 'center',
marginBottom: 40,
},
logoBox: { logoBox: {
width: 64, width: 64, height: 64, backgroundColor: '#003B7E',
height: 64, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginBottom: 16,
backgroundColor: '#003B7E',
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
logoLetter: {
color: '#FFFFFF',
fontSize: 30,
fontWeight: '900',
},
appName: {
fontSize: 30,
fontWeight: '800',
color: '#0F172A',
letterSpacing: -0.6,
marginBottom: 4,
},
tagline: {
fontSize: 14,
color: '#64748B',
textAlign: 'center',
},
form: {
gap: 12,
},
inputLabel: {
fontSize: 14,
fontWeight: '700',
color: '#334155',
}, },
logoLetter: { color: '#FFFFFF', fontSize: 30, fontWeight: '900' },
appName: { fontSize: 30, fontWeight: '800', color: '#0F172A', letterSpacing: -0.6, marginBottom: 4 },
tagline: { fontSize: 14, color: '#64748B', textAlign: 'center' },
form: { gap: 8 },
inputLabel: { fontSize: 14, fontWeight: '700', color: '#334155' },
inputWrap: { inputWrap: {
flexDirection: 'row', flexDirection: 'row', alignItems: 'center',
alignItems: 'center', backgroundColor: '#F8FAFC', borderRadius: 14,
backgroundColor: '#F8FAFC', borderWidth: 1, borderColor: '#E2E8F0',
borderRadius: 14, paddingHorizontal: 12, gap: 8,
borderWidth: 1,
borderColor: '#E2E8F0',
paddingHorizontal: 12,
gap: 8,
},
input: {
flex: 1,
paddingVertical: 13,
color: '#0F172A',
fontSize: 15,
}, },
input: { flex: 1, paddingVertical: 13, color: '#0F172A', fontSize: 15 },
errorBox: { errorBox: {
backgroundColor: '#FEF2F2', backgroundColor: '#FEF2F2', borderWidth: 1,
borderWidth: 1, borderColor: '#FECACA', borderRadius: 12,
borderColor: '#FECACA', paddingHorizontal: 14, paddingVertical: 10,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 10,
},
errorText: {
color: '#B91C1C',
fontSize: 13,
}, },
errorText: { color: '#B91C1C', fontSize: 13 },
submitBtn: { submitBtn: {
backgroundColor: '#003B7E', backgroundColor: '#003B7E', borderRadius: 14,
borderRadius: 14, paddingVertical: 14, alignItems: 'center', marginTop: 8,
paddingVertical: 14,
alignItems: 'center',
marginTop: 4,
},
submitBtnDisabled: {
backgroundColor: '#CBD5E1',
},
submitContent: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
submitLabel: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: 15,
},
hint: {
marginTop: 24,
textAlign: 'center',
color: '#64748B',
fontSize: 13,
lineHeight: 18,
}, },
submitBtnDisabled: { backgroundColor: '#CBD5E1' },
submitContent: { flexDirection: 'row', alignItems: 'center', gap: 6 },
submitLabel: { color: '#FFFFFF', fontWeight: '700', fontSize: 15 },
hint: { marginTop: 24, textAlign: 'center', color: '#64748B', fontSize: 13, lineHeight: 18 },
}) })

View File

@ -2,6 +2,7 @@ import '../global.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Stack, SplashScreen } from 'expo-router' import { Stack, SplashScreen } from 'expo-router'
import { useAuthStore } from '@/store/auth.store' import { useAuthStore } from '@/store/auth.store'
import { TRPCProvider } from '@/lib/trpc'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@ -16,11 +17,13 @@ export default function RootLayout() {
if (!isInitialized) return null if (!isInitialized) return null
return ( return (
<TRPCProvider>
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" /> <Stack.Screen name="index" />
<Stack.Screen name="(auth)" options={{ animation: 'fade' }} /> <Stack.Screen name="(auth)" options={{ animation: 'fade' }} />
<Stack.Screen name="(app)" options={{ animation: 'fade' }} /> <Stack.Screen name="(app)" options={{ animation: 'fade' }} />
<Stack.Screen name="stellen-public/index" /> <Stack.Screen name="stellen-public/index" />
</Stack> </Stack>
</TRPCProvider>
) )
} }

View File

@ -12,10 +12,7 @@ export function useAuth() {
return { return {
session, session,
orgId: 'org-1', isAuthenticated: !!session,
role: 'member' as const,
isAuthenticated: true, // Mock: immer eingeloggt
isAdmin: false,
signOut: handleSignOut, signOut: handleSignOut,
} }
} }

View File

@ -1,31 +1,25 @@
import { MOCK_MEMBERS } from '@/lib/mock-data' import { trpc } from '@/lib/trpc'
import { useMembersFilterStore } from '@/store/members.store' import { useMembersFilterStore } from '@/store/members.store'
export function useMembersList() { export function useMembersList() {
const search = useMembersFilterStore((s) => s.search) const search = useMembersFilterStore((s) => s.search)
const nurAusbildungsbetriebe = useMembersFilterStore((s) => s.nurAusbildungsbetriebe) const nurAusbildungsbetriebe = useMembersFilterStore((s) => s.nurAusbildungsbetriebe)
let data = MOCK_MEMBERS.filter((m) => m.status === 'aktiv') const { data, isLoading, refetch, isFetching } = trpc.members.list.useQuery({
search: search || undefined,
status: 'aktiv',
ausbildungsbetrieb: nurAusbildungsbetriebe || undefined,
})
if (search) { return {
const q = search.toLowerCase() data: data ?? [],
data = data.filter( isLoading,
(m) => refetch,
m.name.toLowerCase().includes(q) || isRefetching: isFetching,
m.betrieb.toLowerCase().includes(q) ||
m.ort.toLowerCase().includes(q) ||
m.sparte.toLowerCase().includes(q)
)
} }
if (nurAusbildungsbetriebe) {
data = data.filter((m) => m.istAusbildungsbetrieb)
}
return { data, isLoading: false, refetch: () => {}, isRefetching: false }
} }
export function useMemberDetail(id: string) { export function useMemberDetail(id: string) {
const data = MOCK_MEMBERS.find((m) => m.id === id) ?? null const { data, isLoading } = trpc.members.byId.useQuery({ id })
return { data, isLoading: false } return { data: data ?? null, isLoading }
} }

View File

@ -1,28 +1,28 @@
import { useState } from 'react' import { trpc } from '@/lib/trpc'
import { MOCK_NEWS } from '@/lib/mock-data'
import { useNewsReadStore } from '@/store/news.store'
export function useNewsList(kategorie?: string) { export function useNewsList(kategorie?: string) {
const localReadIds = useNewsReadStore((s) => s.readIds) const { data, isLoading, refetch, isFetching } = trpc.news.list.useQuery({
const filtered = kategorie kategorie: kategorie || undefined,
? MOCK_NEWS.filter((n) => n.kategorie === kategorie) })
: MOCK_NEWS
const data = filtered.map((n) => ({ return {
...n, data: data ?? [],
isRead: n.isRead || localReadIds.has(n.id), isLoading,
})) refetch,
isRefetching: isFetching,
return { data, isLoading: false, refetch: () => {}, isRefetching: false } }
} }
export function useNewsDetail(id: string) { export function useNewsDetail(id: string) {
const markRead = useNewsReadStore((s) => s.markRead) const utils = trpc.useUtils()
const news = MOCK_NEWS.find((n) => n.id === id) ?? null const { data, isLoading } = trpc.news.byId.useQuery({ id })
const markReadMutation = trpc.news.markRead.useMutation()
function onOpen() { function onOpen() {
markRead(id) markReadMutation.mutate({ newsId: id })
utils.news.list.invalidate()
} }
return { data: news, isLoading: false, onOpen } return { data: data ?? null, isLoading, onOpen }
} }

View File

@ -1,13 +1,20 @@
import { MOCK_STELLEN } from '@/lib/mock-data' import { trpc } from '@/lib/trpc'
export function useStellenListe(opts?: { sparte?: string; lehrjahr?: string }) { export function useStellenListe(opts?: { sparte?: string; lehrjahr?: string }) {
let data = MOCK_STELLEN.filter((s) => s.aktiv) const { data, isLoading, refetch, isFetching } = trpc.stellen.listPublic.useQuery({
if (opts?.sparte) data = data.filter((s) => s.sparte === opts.sparte) sparte: opts?.sparte,
if (opts?.lehrjahr) data = data.filter((s) => s.lehrjahr === opts.lehrjahr) lehrjahr: opts?.lehrjahr,
return { data, isLoading: false, refetch: () => {}, isRefetching: false } })
return {
data: data ?? [],
isLoading,
refetch,
isRefetching: isFetching,
}
} }
export function useStelleDetail(id: string) { export function useStelleDetail(id: string) {
const data = MOCK_STELLEN.find((s) => s.id === id) ?? null const { data, isLoading } = trpc.stellen.byId.useQuery({ id })
return { data, isLoading: false } return { data: data ?? null, isLoading }
} }

View File

@ -1,33 +1,32 @@
import { useState } from 'react' import { trpc } from '@/lib/trpc'
import { MOCK_TERMINE } from '@/lib/mock-data'
export function useTermineListe(upcoming = true) { export function useTermineListe(upcoming = true) {
const now = new Date() const { data, isLoading, refetch, isFetching } = trpc.termine.list.useQuery({ upcoming })
const data = MOCK_TERMINE.filter((t) =>
upcoming ? t.datum >= now : t.datum < now return {
).sort((a, b) => data: data ?? [],
upcoming ? a.datum.getTime() - b.datum.getTime() : b.datum.getTime() - a.datum.getTime() isLoading,
) refetch,
return { data, isLoading: false, refetch: () => {}, isRefetching: false } isRefetching: isFetching,
}
} }
export function useTerminDetail(id: string) { export function useTerminDetail(id: string) {
const data = MOCK_TERMINE.find((t) => t.id === id) ?? null const { data, isLoading } = trpc.termine.byId.useQuery({ id })
return { data, isLoading: false } return { data: data ?? null, isLoading }
} }
export function useToggleAnmeldung() { export function useToggleAnmeldung() {
const [isPending, setIsPending] = useState(false) const utils = trpc.useUtils()
const mutation = trpc.termine.toggleAnmeldung.useMutation({
onSuccess: () => {
utils.termine.list.invalidate()
utils.termine.byId.invalidate()
},
})
function mutate({ terminId }: { terminId: string }) { return {
setIsPending(true) mutate: ({ terminId }: { terminId: string }) => mutation.mutate({ terminId }),
const termin = MOCK_TERMINE.find((t) => t.id === terminId) isPending: mutation.isPending,
if (termin) {
termin.isAngemeldet = !termin.isAngemeldet
termin.teilnehmerAnzahl += termin.isAngemeldet ? 1 : -1
} }
setTimeout(() => setIsPending(false), 300)
}
return { mutate, isPending }
} }

View File

@ -1,6 +1,7 @@
import { createAuthClient } from 'better-auth/react' import { createAuthClient } from 'better-auth/react'
import { magicLinkClient } from 'better-auth/client/plugins' import { magicLinkClient } from 'better-auth/client/plugins'
import Constants from 'expo-constants' import Constants from 'expo-constants'
import AsyncStorage from '@react-native-async-storage/async-storage'
const apiUrl = const apiUrl =
Constants.expoConfig?.extra?.apiUrl ?? Constants.expoConfig?.extra?.apiUrl ??
@ -10,4 +11,15 @@ const apiUrl =
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: apiUrl, baseURL: apiUrl,
plugins: [magicLinkClient()], plugins: [magicLinkClient()],
fetchOptions: {
customFetchImpl: async (url, options) => {
const token = await AsyncStorage.getItem('better-auth-session')
const headers = new Headers((options?.headers as HeadersInit) ?? {})
headers.set('origin', apiUrl)
if (token) {
headers.set('cookie', `better-auth.session_token=${token}`)
}
return fetch(url, { ...options, headers })
},
},
}) })

View File

@ -1,5 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { MOCK_MEMBER_ME } from '@/lib/mock-data' import { authClient } from '@/lib/auth-client'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface Session { interface Session {
user: { id: string; email: string; name: string } user: { id: string; email: string; name: string }
@ -9,26 +10,48 @@ interface AuthState {
session: Session | null session: Session | null
isInitialized: boolean isInitialized: boolean
initialize: () => Promise<void> initialize: () => Promise<void>
setSession: (session: Session | null, token?: string) => Promise<void>
signOut: () => Promise<void> signOut: () => Promise<void>
} }
export const useAuthStore = create<AuthState>((set) => ({ export const useAuthStore = create<AuthState>((set) => ({
// Mock: direkt eingeloggt session: null,
session: { isInitialized: false,
user: {
id: MOCK_MEMBER_ME.userId!,
email: MOCK_MEMBER_ME.email,
name: MOCK_MEMBER_ME.name,
},
},
isInitialized: true,
initialize: async () => { initialize: async () => {
// Mock: nichts zu tun try {
set({ isInitialized: true }) // Check if we have a stored token and validate it
const token = await AsyncStorage.getItem('better-auth-session')
if (!token) {
set({ session: null, isInitialized: true })
return
}
// authClient now sends the token via cookie header (see auth-client.ts)
const result = await authClient.getSession()
if (result?.data?.user) {
set({
session: { user: result.data.user },
isInitialized: true,
})
} else {
await AsyncStorage.removeItem('better-auth-session')
set({ session: null, isInitialized: true })
}
} catch {
set({ session: null, isInitialized: true })
}
},
setSession: async (session, token) => {
if (token) {
await AsyncStorage.setItem('better-auth-session', token)
}
set({ session })
}, },
signOut: async () => { signOut: async () => {
await authClient.signOut()
await AsyncStorage.removeItem('better-auth-session')
set({ session: null }) set({ session: null })
}, },
})) }))

View File

@ -6,12 +6,12 @@
"build": "turbo build", "build": "turbo build",
"lint": "turbo lint", "lint": "turbo lint",
"type-check": "turbo type-check", "type-check": "turbo type-check",
"db:generate": "pnpm --filter @innungsapp/shared prisma generate", "db:generate": "pnpm --filter @innungsapp/shared prisma:generate",
"db:migrate": "pnpm --filter @innungsapp/shared prisma migrate dev", "db:migrate": "pnpm --filter @innungsapp/shared prisma:migrate",
"db:push": "pnpm --filter @innungsapp/shared prisma db push", "db:push": "pnpm --filter @innungsapp/shared prisma:push",
"db:studio": "pnpm --filter @innungsapp/shared prisma studio", "db:studio": "pnpm --filter @innungsapp/shared prisma:studio",
"db:seed": "pnpm --filter @innungsapp/shared tsx prisma/seed.ts", "db:seed": "pnpm --filter @innungsapp/shared prisma:seed",
"db:reset": "pnpm --filter @innungsapp/shared prisma migrate reset" "db:reset": "pnpm --filter @innungsapp/shared prisma:migrate -- --reset"
}, },
"devDependencies": { "devDependencies": {
"turbo": "^2.3.0", "turbo": "^2.3.0",

View File

Binary file not shown.

View File

@ -1,12 +1,14 @@
// InnungsApp — Prisma Schema // InnungsApp — Prisma Schema
// Stack: PostgreSQL + Prisma ORM + better-auth // Stack: SQLite + Prisma ORM + better-auth
// Note: SQLite has no native enum support — enum fields are stored as String.
// Valid values are enforced at the application layer (Zod).
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
datasource db { datasource db {
provider = "postgresql" provider = "sqlite"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
@ -90,7 +92,7 @@ model Organization {
id String @id @default(uuid()) id String @id @default(uuid())
name String name String
slug String @unique slug String @unique
plan Plan @default(pilot) plan String @default("pilot") // pilot | standard | pro | verband
logoUrl String? @map("logo_url") logoUrl String? @map("logo_url")
primaryColor String @default("#E63946") @map("primary_color") primaryColor String @default("#E63946") @map("primary_color")
contactEmail String? @map("contact_email") contactEmail String? @map("contact_email")
@ -107,13 +109,6 @@ model Organization {
@@map("organizations") @@map("organizations")
} }
enum Plan {
pilot
standard
pro
verband
}
// ============================================= // =============================================
// MEMBERS // MEMBERS
// ============================================= // =============================================
@ -128,7 +123,7 @@ model Member {
ort String ort String
telefon String? telefon String?
email String email String
status MemberStatus @default(aktiv) status String @default("aktiv") // aktiv | ruhend | ausgetreten
istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb") istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb")
seit Int? seit Int?
avatarUrl String? @map("avatar_url") avatarUrl String? @map("avatar_url")
@ -147,12 +142,6 @@ model Member {
@@map("members") @@map("members")
} }
enum MemberStatus {
aktiv
ruhend
ausgetreten
}
// ============================================= // =============================================
// USER ROLES (multi-tenancy) // USER ROLES (multi-tenancy)
// ============================================= // =============================================
@ -161,7 +150,7 @@ model UserRole {
id String @id @default(uuid()) id String @id @default(uuid())
orgId String @map("org_id") orgId String @map("org_id")
userId String @map("user_id") userId String @map("user_id")
role OrgRole role String // admin | member
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
@ -171,11 +160,6 @@ model UserRole {
@@map("user_roles") @@map("user_roles")
} }
enum OrgRole {
admin
member
}
// ============================================= // =============================================
// NEWS // NEWS
// ============================================= // =============================================
@ -186,7 +170,7 @@ model News {
authorId String? @map("author_id") authorId String? @map("author_id")
title String title String
body String // Markdown body String // Markdown
kategorie NewsKategorie kategorie String // Wichtig | Pruefung | Foerderung | Veranstaltung | Allgemein
publishedAt DateTime? @map("published_at") // NULL = Entwurf publishedAt DateTime? @map("published_at") // NULL = Entwurf
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
@ -200,14 +184,6 @@ model News {
@@map("news") @@map("news")
} }
enum NewsKategorie {
Wichtig
Pruefung
Foerderung
Veranstaltung
Allgemein
}
model NewsRead { model NewsRead {
id String @id @default(uuid()) id String @id @default(uuid())
newsId String @map("news_id") newsId String @map("news_id")
@ -269,13 +245,13 @@ model Termin {
id String @id @default(uuid()) id String @id @default(uuid())
orgId String @map("org_id") orgId String @map("org_id")
titel String titel String
datum DateTime @db.Date datum DateTime
uhrzeit String? // stored as "HH:MM" uhrzeit String? // stored as "HH:MM"
endeDatum DateTime? @map("ende_datum") @db.Date endeDatum DateTime? @map("ende_datum")
endeUhrzeit String? @map("ende_uhrzeit") endeUhrzeit String? @map("ende_uhrzeit")
ort String? ort String?
adresse String? adresse String?
typ TerminTyp typ String // Pruefung | Versammlung | Kurs | Event | Sonstiges
beschreibung String? beschreibung String?
maxTeilnehmer Int? @map("max_teilnehmer") // NULL = unbegrenzt maxTeilnehmer Int? @map("max_teilnehmer") // NULL = unbegrenzt
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
@ -288,14 +264,6 @@ model Termin {
@@map("termine") @@map("termine")
} }
enum TerminTyp {
Pruefung
Versammlung
Kurs
Event
Sonstiges
}
model TerminAnmeldung { model TerminAnmeldung {
id String @id @default(uuid()) id String @id @default(uuid())
terminId String @map("termin_id") terminId String @map("termin_id")

View File

@ -2,6 +2,12 @@ import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
// bcrypt-compatible hash using better-auth's default (sha256 fallback for seeding)
// better-auth uses its own hashing — we use the auth API to set a real password instead.
// For seeding we insert a known bcrypt hash for "demo1234".
// Generated with: https://bcrypt-generator.com/ (rounds=10)
const DEMO_PASSWORD_HASH = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lHny'
async function main() { async function main() {
console.log('Seeding database...') console.log('Seeding database...')
@ -33,6 +39,19 @@ async function main() {
}, },
}) })
// Create password account so email+password login works in dev
await prisma.account.upsert({
where: { id: 'demo-admin-account-id' },
update: {},
create: {
id: 'demo-admin-account-id',
accountId: adminUser.id,
providerId: 'credential',
userId: adminUser.id,
password: DEMO_PASSWORD_HASH,
},
})
await prisma.userRole.upsert({ await prisma.userRole.upsert({
where: { orgId_userId: { orgId: org.id, userId: adminUser.id } }, where: { orgId_userId: { orgId: org.id, userId: adminUser.id } },
update: {}, update: {},

View File

@ -12,13 +12,16 @@ export type {
Stelle, Stelle,
Termin, Termin,
TerminAnmeldung, TerminAnmeldung,
Plan,
MemberStatus,
OrgRole,
NewsKategorie,
TerminTyp,
} from '@prisma/client' } from '@prisma/client'
// SQLite has no native enum support — define string union types manually.
// These mirror the valid values stored in the DB (enforced via Zod at the API layer).
export type Plan = 'pilot' | 'standard' | 'pro' | 'verband'
export type MemberStatus = 'aktiv' | 'ruhend' | 'ausgetreten'
export type OrgRole = 'admin' | 'member'
export type NewsKategorie = 'Wichtig' | 'Pruefung' | 'Foerderung' | 'Veranstaltung' | 'Allgemein'
export type TerminTyp = 'Pruefung' | 'Versammlung' | 'Kurs' | 'Event' | 'Sonstiges'
// ============================================= // =============================================
// UI Display Helpers // UI Display Helpers
// ============================================= // =============================================

View File

@ -40,7 +40,7 @@ importers:
version: 4.0.11(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 4.0.11(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
better-auth: better-auth:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@ -51,8 +51,8 @@ importers:
specifier: ^0.460.0 specifier: ^0.460.0
version: 0.460.0(react@18.3.1) version: 0.460.0(react@18.3.1)
next: next:
specifier: ^15.0.0 specifier: 15.3.4
version: 15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nodemailer: nodemailer:
specifier: ^6.9.0 specifier: ^6.9.0
version: 6.10.1 version: 6.10.1
@ -1355,54 +1355,105 @@ packages:
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@next/env@15.3.4':
resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==}
'@next/env@15.5.12': '@next/env@15.5.12':
resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==}
'@next/eslint-plugin-next@15.5.12': '@next/eslint-plugin-next@15.5.12':
resolution: {integrity: sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==} resolution: {integrity: sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==}
'@next/swc-darwin-arm64@15.3.4':
resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-arm64@15.5.12': '@next/swc-darwin-arm64@15.5.12':
resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==} resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@15.3.4':
resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-darwin-x64@15.5.12': '@next/swc-darwin-x64@15.5.12':
resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==} resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@15.3.4':
resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-gnu@15.5.12': '@next/swc-linux-arm64-gnu@15.5.12':
resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==} resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-arm64-musl@15.3.4':
resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.5.12': '@next/swc-linux-arm64-musl@15.5.12':
resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==} resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-x64-gnu@15.3.4':
resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@15.5.12': '@next/swc-linux-x64-gnu@15.5.12':
resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==} resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-linux-x64-musl@15.3.4':
resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.5.12': '@next/swc-linux-x64-musl@15.5.12':
resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==} resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-win32-arm64-msvc@15.3.4':
resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-arm64-msvc@15.5.12': '@next/swc-win32-arm64-msvc@15.5.12':
resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==} resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@15.3.4':
resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@next/swc-win32-x64-msvc@15.5.12': '@next/swc-win32-x64-msvc@15.5.12':
resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==} resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@ -1814,6 +1865,9 @@ packages:
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@ -2484,6 +2538,10 @@ packages:
buffer@5.7.1: buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2: bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -4375,6 +4433,28 @@ packages:
nested-error-stacks@2.0.1: nested-error-stacks@2.0.1:
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
next@15.3.4:
resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
babel-plugin-react-compiler:
optional: true
sass:
optional: true
next@15.5.12: next@15.5.12:
resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@ -5224,6 +5304,10 @@ packages:
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
strict-uri-encode@2.0.0: strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -7123,33 +7207,60 @@ snapshots:
'@tybys/wasm-util': 0.10.1 '@tybys/wasm-util': 0.10.1
optional: true optional: true
'@next/env@15.5.12': {} '@next/env@15.3.4': {}
'@next/env@15.5.12':
optional: true
'@next/eslint-plugin-next@15.5.12': '@next/eslint-plugin-next@15.5.12':
dependencies: dependencies:
fast-glob: 3.3.1 fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.3.4':
optional: true
'@next/swc-darwin-arm64@15.5.12': '@next/swc-darwin-arm64@15.5.12':
optional: true optional: true
'@next/swc-darwin-x64@15.3.4':
optional: true
'@next/swc-darwin-x64@15.5.12': '@next/swc-darwin-x64@15.5.12':
optional: true optional: true
'@next/swc-linux-arm64-gnu@15.3.4':
optional: true
'@next/swc-linux-arm64-gnu@15.5.12': '@next/swc-linux-arm64-gnu@15.5.12':
optional: true optional: true
'@next/swc-linux-arm64-musl@15.3.4':
optional: true
'@next/swc-linux-arm64-musl@15.5.12': '@next/swc-linux-arm64-musl@15.5.12':
optional: true optional: true
'@next/swc-linux-x64-gnu@15.3.4':
optional: true
'@next/swc-linux-x64-gnu@15.5.12': '@next/swc-linux-x64-gnu@15.5.12':
optional: true optional: true
'@next/swc-linux-x64-musl@15.3.4':
optional: true
'@next/swc-linux-x64-musl@15.5.12': '@next/swc-linux-x64-musl@15.5.12':
optional: true optional: true
'@next/swc-win32-arm64-msvc@15.3.4':
optional: true
'@next/swc-win32-arm64-msvc@15.5.12': '@next/swc-win32-arm64-msvc@15.5.12':
optional: true optional: true
'@next/swc-win32-x64-msvc@15.3.4':
optional: true
'@next/swc-win32-x64-msvc@15.5.12': '@next/swc-win32-x64-msvc@15.5.12':
optional: true optional: true
@ -7600,6 +7711,8 @@ snapshots:
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -8269,6 +8382,27 @@ snapshots:
bcp-47-match@2.0.3: {} bcp-47-match@2.0.3: {}
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.1.8(zod@4.3.6)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.0
zod: 4.3.6
optionalDependencies:
'@prisma/client': 5.22.0(prisma@5.22.0)
next: 15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
prisma: 5.22.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0))(prisma@5.22.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0): better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0))(prisma@5.22.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
@ -8290,27 +8424,6 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-dom: 18.3.1(react@19.1.0) react-dom: 18.3.1(react@19.1.0)
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.1.8(zod@4.3.6)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.0
zod: 4.3.6
optionalDependencies:
'@prisma/client': 5.22.0(prisma@5.22.0)
next: 15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
prisma: 5.22.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
better-call@1.1.8(zod@4.3.6): better-call@1.1.8(zod@4.3.6):
dependencies: dependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
@ -8378,6 +8491,10 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {} bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
@ -10822,6 +10939,32 @@ snapshots:
nested-error-stacks@2.0.1: {} nested-error-stacks@2.0.1: {}
next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.3.4
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001770
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.6(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.3.4
'@next/swc-darwin-x64': 15.3.4
'@next/swc-linux-arm64-gnu': 15.3.4
'@next/swc-linux-arm64-musl': 15.3.4
'@next/swc-linux-x64-gnu': 15.3.4
'@next/swc-linux-x64-musl': 15.3.4
'@next/swc-win32-arm64-msvc': 15.3.4
'@next/swc-win32-x64-msvc': 15.3.4
babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0): next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@next/env': 15.5.12 '@next/env': 15.5.12
@ -10847,30 +10990,6 @@ snapshots:
- babel-plugin-macros - babel-plugin-macros
optional: true optional: true
next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.5.12
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001770
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.6(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.5.12
'@next/swc-darwin-x64': 15.5.12
'@next/swc-linux-arm64-gnu': 15.5.12
'@next/swc-linux-arm64-musl': 15.5.12
'@next/swc-linux-x64-gnu': 15.5.12
'@next/swc-linux-x64-musl': 15.5.12
'@next/swc-win32-arm64-msvc': 15.5.12
'@next/swc-win32-x64-msvc': 15.5.12
babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
node-exports-info@1.6.0: node-exports-info@1.6.0:
dependencies: dependencies:
array.prototype.flatmap: 1.3.3 array.prototype.flatmap: 1.3.3
@ -11870,6 +11989,8 @@ snapshots:
stream-buffers@2.2.0: {} stream-buffers@2.2.0: {}
streamsearch@1.1.0: {}
strict-uri-encode@2.0.0: {} strict-uri-encode@2.0.0: {}
string-width@4.2.3: string-width@4.2.3:

View File

@ -0,0 +1,37 @@
Region,Organisation,URL,Kontaktperson,Email,Telefon,Anmerkung
Köln,Kreishandwerkerschaft Köln,www.handwerk.koeln,Roberto Lepore (Hauptgeschäftsführer) / Nicolai Lucks (Kreishandwerksmeister),lepore@handwerk.koeln,,
Köln,Büchsenmacher-Innung Nordrhein / RLP / Saarland,,(kein Website),kliedl@t-online.de,,Klaus-Bernd Liedl (Obermeister)
Köln,Fleischer-Innung Köln,,(kein Website),obermeister@fleischer-koeln.de,,Astrid Schmitz (Obermeisterin)
Köln,Glaser-Innung Köln-Bonn-Aachen,,(kein Website),mail@glas-bong.de,,Anne Bong (Obermeisterin)
Köln,Juwelier- / Gold- und Silberschmiede-Innung Köln,,(kein Website),info@sotos-schmuck.de,,Ingo Telkmann (Obermeister)
Köln,Innung Farbe Köln,,(kein Website),s.epe@epe-maler.de,,Sebastian Epe (Obermeister)
Köln,Innung des Maßschneiderhandwerks Köln / Textiliniger-Innung Köln/Bonn,,(kein Website),twp.koeln@gmail.com,,Thomas Wien-Pegelow (Obermeister)
Köln,Innung für Metalltechnik Köln,,(kein Website),info@van-broek.de,,Sascha Franke (Obermeister)
Köln,Innung für Orthopädie-Technik Köln,,(kein Website),sebastian@malzkorn.at,,Sebastian Malzkorn (Obermeister)
Köln,Raumausstatter-Innung Köln,,(kein Website),info@diana-breidenbach.de,,Diana Goeddertz (Obermeisterin)
Köln,Innung Köln Rollladen und Sonnenschutz,,(kein Website),info@rhp-online.de,,Andre Urban (Obermeister)
Köln,Stuckateur-Innung Köln - Ausbau + Fassade,,(kein Website),s.rettig@hhhuerth.de,,Sarah M. Rettig (Obermeisterin)
Köln,Werbetechniker-Innung Köln-Bonn-Aachen,,(kein Website),info@werbetechnik-baecker.de,,Markus Böcker (Obermeister)
Köln,Augenoptiker-Innung Köln-Aachen,www.optikerinnung.de/aoi/,Hans Josef Schümmer (Obermeister),info@optikerinnung.de,,LinkedIn vorhanden
Köln,Dachdecker- und Zimmerer-Innung Köln,www.dachdecker-innung-koeln.de,Oliver Miesen (Obermeister) / Bettina Dietrich (Geschäftsführerin),e-mail@dachdecker-innung-koeln.de,,Dachdecker und Zimmerer teilen sich eine Domain
Köln,Elektroinnung Köln,www.elektroinnungkoeln.de,Etienne Berndt (Geschäftsführer) / Ralf Janowski (Obermeister),info@elektroinnungkoeln.de,0221 123071,Facebook + Instagram vorhanden
Köln,Friseur-Innung Köln,www.kopfarbeit-koeln.de,Mike Engels (Obermeister) / Julia Barth (Geschäftsführerin),info@kopfarbeit-koeln.de,,
Köln,Innung des Gebäudereiniger-Handwerks Köln-Aachen,www.die-gebaeudedienstleister-koeln-aachen.de,Detlef Ptak (Obermeister) / Jennifer Schramm (Geschäftsführerin),info@gebaeudereiniger-koeln-aachen.de,0221 251064,Anschrift: Frankenwerft 35; 50667 Köln
Köln,Bundesinnung für das Gerüstbauer-Handwerk,www.geruestbauhandwerk.de,Marcus Nachbauer (Bundesinnungsmeister) / Sabrina Luther (Geschäftsführerin),info@geruestbauhandwerk.de,,Bundesinnung (Sitz Köln)
Köln,Innung für Informationstechnik Köln/Bonn/Rhein-Sieg/Rhein-Erft,,(kein Website),n.gassner@koenig-avt.de,,Nicolay Gassner (Obermeister)
Köln,Karosseriebauer-Innung Köln,www.karosserie-innungkoeln.de,Oliver Nienhaus (Obermeister) / Claudia Weiler (Geschäftsführerin),info@karosserie-innungkoeln.de,,Facebook vorhanden
Köln,Konditoren-Innung Köln-Bonn,,(kein Website),info@cafe-schoener.de,,Rudolf Schöner (Obermeister)
Köln,Innung Sanitär Heizung Klima Köln,www.shk-innung-koeln.de,N/A (Leitbild vorhanden),info@shk-innung-koeln.de,(0221) 83712-0,Rolshover Str. 115; 51105 Köln
Düsseldorf,Kreishandwerkerschaft Düsseldorf,www.kh-duesseldorf.de,(kein Email auf Website),—,0211-36 70 70,Klosterstraße 73-75; 40211 Düsseldorf; kein Email auf Website gefunden
Düsseldorf,Dachdecker-Innung Düsseldorf,www.dachdeckerinnung.nrw,N/A,info@dachdeckerinnung.nrw,0211 / 36 70 710,Klosterstraße 73-75; 40211 Düsseldorf (gleiche Adresse wie KH DUS)
Düsseldorf,Innung des Kraftfahrzeuggewerbes Düsseldorf,www.kfz-innung-duesseldorf.de,Sven Gustavson (GF) / Hermann Görtz (Obermeister),info@kfz-innung-duesseldorf.de,+49 211-6634 34,Facebook + Instagram vorhanden
Düsseldorf,Innung für Sanitär- und Heizungstechnik Düsseldorf,www.shk-duesseldorf.de,Horst Jansen (GF) / Hans Werner Eschrich (Obermeister),info@shk-duesseldorf.de,0211 36707-10,
Düsseldorf,Augenoptiker-Innung Düssel-Rhein-Ruhr,,(kein Website bekannt),—,,Jens Schulz (Obermeister); kein Email gefunden
Düsseldorf,Verband des Rheinischen Bäckerhandwerks,,(kein Website bekannt),—,,Henning Funke (GF) / Johannes Dackweiler (Obermeister); kein Email gefunden
Düsseldorf,Baugewerbe-Innung Düsseldorf,,(kein Website bekannt),—,,Peter Szemenyei (GF) / Christoph Morick (Obermeister); kein Email gefunden
Düsseldorf,Bestatter-Innung NRW,,(kein Website bekannt),—,,Christian Jaeger (GF) / Frank Wesemann (Obermeister); kein Email gefunden
Düsseldorf,Fleischer-Innung Düsseldorf-Mettmann-Solingen,,(kein Website bekannt),—,,Daniela van der Valk (GF) / Lutz Kluke (Obermeister); kein Email gefunden
Düsseldorf,Innung für Orthopädie-Schuhtechnik Rheinland/Westfalen,,(kein Website bekannt),—,,Irene Zamponi (GF) / Philipp Radtke (Obermeister); kein Email gefunden
Düsseldorf,Schornsteinfeger-Innung Regierungsbezirk Düsseldorf,,(kein Website bekannt),—,,Marcus Dörenkamp (GF); kein Email gefunden
Düsseldorf,Stukkatuer-Innung Wuppertal und Kreis Mettmann,,(kein Website bekannt),—,,Hermann Schulte-Hiltrop (HGF) / Wolfgang Wüstenhagen (Obermeister); kein Email gefunden
Düsseldorf,Zahntechniker-Innung Düsseldorf,,(kein Website bekannt),—,,Michael Knittel (GF) / Dominik Kruchen (Obermeister); kein Email gefunden
1 Region Organisation URL Kontaktperson Email Telefon Anmerkung
2 Köln Kreishandwerkerschaft Köln www.handwerk.koeln Roberto Lepore (Hauptgeschäftsführer) / Nicolai Lucks (Kreishandwerksmeister) lepore@handwerk.koeln
3 Köln Büchsenmacher-Innung Nordrhein / RLP / Saarland (kein Website) kliedl@t-online.de Klaus-Bernd Liedl (Obermeister)
4 Köln Fleischer-Innung Köln (kein Website) obermeister@fleischer-koeln.de Astrid Schmitz (Obermeisterin)
5 Köln Glaser-Innung Köln-Bonn-Aachen (kein Website) mail@glas-bong.de Anne Bong (Obermeisterin)
6 Köln Juwelier- / Gold- und Silberschmiede-Innung Köln (kein Website) info@sotos-schmuck.de Ingo Telkmann (Obermeister)
7 Köln Innung Farbe Köln (kein Website) s.epe@epe-maler.de Sebastian Epe (Obermeister)
8 Köln Innung des Maßschneiderhandwerks Köln / Textiliniger-Innung Köln/Bonn (kein Website) twp.koeln@gmail.com Thomas Wien-Pegelow (Obermeister)
9 Köln Innung für Metalltechnik Köln (kein Website) info@van-broek.de Sascha Franke (Obermeister)
10 Köln Innung für Orthopädie-Technik Köln (kein Website) sebastian@malzkorn.at Sebastian Malzkorn (Obermeister)
11 Köln Raumausstatter-Innung Köln (kein Website) info@diana-breidenbach.de Diana Goeddertz (Obermeisterin)
12 Köln Innung Köln Rollladen und Sonnenschutz (kein Website) info@rhp-online.de Andre Urban (Obermeister)
13 Köln Stuckateur-Innung Köln - Ausbau + Fassade (kein Website) s.rettig@hhhuerth.de Sarah M. Rettig (Obermeisterin)
14 Köln Werbetechniker-Innung Köln-Bonn-Aachen (kein Website) info@werbetechnik-baecker.de Markus Böcker (Obermeister)
15 Köln Augenoptiker-Innung Köln-Aachen www.optikerinnung.de/aoi/ Hans Josef Schümmer (Obermeister) info@optikerinnung.de LinkedIn vorhanden
16 Köln Dachdecker- und Zimmerer-Innung Köln www.dachdecker-innung-koeln.de Oliver Miesen (Obermeister) / Bettina Dietrich (Geschäftsführerin) e-mail@dachdecker-innung-koeln.de Dachdecker und Zimmerer teilen sich eine Domain
17 Köln Elektroinnung Köln www.elektroinnungkoeln.de Etienne Berndt (Geschäftsführer) / Ralf Janowski (Obermeister) info@elektroinnungkoeln.de 0221 123071 Facebook + Instagram vorhanden
18 Köln Friseur-Innung Köln www.kopfarbeit-koeln.de Mike Engels (Obermeister) / Julia Barth (Geschäftsführerin) info@kopfarbeit-koeln.de
19 Köln Innung des Gebäudereiniger-Handwerks Köln-Aachen www.die-gebaeudedienstleister-koeln-aachen.de Detlef Ptak (Obermeister) / Jennifer Schramm (Geschäftsführerin) info@gebaeudereiniger-koeln-aachen.de 0221 251064 Anschrift: Frankenwerft 35; 50667 Köln
20 Köln Bundesinnung für das Gerüstbauer-Handwerk www.geruestbauhandwerk.de Marcus Nachbauer (Bundesinnungsmeister) / Sabrina Luther (Geschäftsführerin) info@geruestbauhandwerk.de Bundesinnung (Sitz Köln)
21 Köln Innung für Informationstechnik Köln/Bonn/Rhein-Sieg/Rhein-Erft (kein Website) n.gassner@koenig-avt.de Nicolay Gassner (Obermeister)
22 Köln Karosseriebauer-Innung Köln www.karosserie-innungkoeln.de Oliver Nienhaus (Obermeister) / Claudia Weiler (Geschäftsführerin) info@karosserie-innungkoeln.de Facebook vorhanden
23 Köln Konditoren-Innung Köln-Bonn (kein Website) info@cafe-schoener.de Rudolf Schöner (Obermeister)
24 Köln Innung Sanitär Heizung Klima Köln www.shk-innung-koeln.de N/A (Leitbild vorhanden) info@shk-innung-koeln.de (0221) 83712-0 Rolshover Str. 115; 51105 Köln
25 Düsseldorf Kreishandwerkerschaft Düsseldorf www.kh-duesseldorf.de (kein Email auf Website) 0211-36 70 70 Klosterstraße 73-75; 40211 Düsseldorf; kein Email auf Website gefunden
26 Düsseldorf Dachdecker-Innung Düsseldorf www.dachdeckerinnung.nrw N/A info@dachdeckerinnung.nrw 0211 / 36 70 710 Klosterstraße 73-75; 40211 Düsseldorf (gleiche Adresse wie KH DUS)
27 Düsseldorf Innung des Kraftfahrzeuggewerbes Düsseldorf www.kfz-innung-duesseldorf.de Sven Gustavson (GF) / Hermann Görtz (Obermeister) info@kfz-innung-duesseldorf.de +49 211-6634 34 Facebook + Instagram vorhanden
28 Düsseldorf Innung für Sanitär- und Heizungstechnik Düsseldorf www.shk-duesseldorf.de Horst Jansen (GF) / Hans Werner Eschrich (Obermeister) info@shk-duesseldorf.de 0211 36707-10
29 Düsseldorf Augenoptiker-Innung Düssel-Rhein-Ruhr (kein Website bekannt) Jens Schulz (Obermeister); kein Email gefunden
30 Düsseldorf Verband des Rheinischen Bäckerhandwerks (kein Website bekannt) Henning Funke (GF) / Johannes Dackweiler (Obermeister); kein Email gefunden
31 Düsseldorf Baugewerbe-Innung Düsseldorf (kein Website bekannt) Peter Szemenyei (GF) / Christoph Morick (Obermeister); kein Email gefunden
32 Düsseldorf Bestatter-Innung NRW (kein Website bekannt) Christian Jaeger (GF) / Frank Wesemann (Obermeister); kein Email gefunden
33 Düsseldorf Fleischer-Innung Düsseldorf-Mettmann-Solingen (kein Website bekannt) Daniela van der Valk (GF) / Lutz Kluke (Obermeister); kein Email gefunden
34 Düsseldorf Innung für Orthopädie-Schuhtechnik Rheinland/Westfalen (kein Website bekannt) Irene Zamponi (GF) / Philipp Radtke (Obermeister); kein Email gefunden
35 Düsseldorf Schornsteinfeger-Innung Regierungsbezirk Düsseldorf (kein Website bekannt) Marcus Dörenkamp (GF); kein Email gefunden
36 Düsseldorf Stukkatuer-Innung Wuppertal und Kreis Mettmann (kein Website bekannt) Hermann Schulte-Hiltrop (HGF) / Wolfgang Wüstenhagen (Obermeister); kein Email gefunden
37 Düsseldorf Zahntechniker-Innung Düsseldorf (kein Website bekannt) Michael Knittel (GF) / Dominik Kruchen (Obermeister); kein Email gefunden

BIN
leads/compare_output.txt Normal file

Binary file not shown.

159
leads/compare_pdf_csv.py Normal file
View File

@ -0,0 +1,159 @@
import csv
# All entries from PDF (name → {contact, email, phone, type})
pdf_entries = {
# Bäcker
"Bäckerinnung Bayerischer Untermain": {"contact": "Veronika Hench", "email": "v.hench@baeckerinnung-bayerischer-untermain.de", "phone": "06021 790777"},
"Bäckerinnung Bad Kissingen - Rhön-Grabfeld": {"contact": "Petra Schwab (Ansp.) / Ullrich Amthor (Obermeister)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Bäckerinnung Kitzingen": {"contact": "Elisabeth Hofmann (Ansp.) / Tilo Brönner (Obermeister)", "email": "", "phone": "09332 590636"},
"Bäckerinnung Schweinfurt - Haßberge": {"contact": "Brigitte Rapp (Ansp.) / Gerhard Götz (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Bäckerinnung Mainfranken": {"contact": "Christine Winterbauer (Ansp.) / Marcel Scherg (Obermeister)", "email": "christine.winterbauer@baeckerbuchstelle.de", "phone": "09302 909431"},
# Bau
"Bauinnung Aschaffenburg": {"contact": "Nina Emeneth (Ansp.) / Felix Englert (Obermeister)", "email": "info@bauinnung-aschaffenburg.de", "phone": "06021 421086"},
"Bauinnung Bad Kissingen / Rhön-Grabfeld": {"contact": "Petra Schwab (Ansp.) / Stefan Goos (Obermeister)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Bau-Innung Schweinfurt": {"contact": "Ramona Ziegler (Ansp.) / Karl Böhner (Obermeister)", "email": "info@bauinnung-schweinfurt.de", "phone": "09721 74220"},
"Bauinnung Mainfranken - Würzburg": {"contact": "Manfred Dallner (Ansp.) / Ralf Stegmeier (Obermeister)", "email": "baugewerbe@lbb-unterfranken.de", "phone": "0931 454440"},
# Bekleidung
"Innung des Bekleidungshandwerks Unterfranken": {"contact": "Nicole Brandler (Ansp.) / Friedrun Schlagbauer-Werner (Obermeisterin)", "email": "info@innung-unterfranken.de", "phone": "09732 2277"},
# Brauer
"Brauer- und Mälzerinnung Unterfranken": {"contact": "Josef Göller (Obermeister)", "email": "info@private-brauereien-bayern.de", "phone": "089 2909560"},
# Dachdecker
"Dachdeckerinnung Aschaffenburg - Miltenberg": {"contact": "Lukas Thalheimer (Obermeister)", "email": "lukas.thalheimer@thalheimer.de", "phone": "0160 8480279"},
"Dachdeckerinnung Unterfranken": {"contact": "Timo Markert (Obermeister)", "email": "info@mein-dachdecker.com", "phone": "09321 3905830"},
# Elektro
"Innung für Elektro- und Informationstechnik Bayerischer Untermain": {"contact": "Annett Kinzel (Ansp.) / Edwin Palzer (Obermeister)", "email": "info@elektroinnung-bayerischeruntermain.de", "phone": "06021 480331"},
"Innung für Elektro- und Informationstechnik Haßberge": {"contact": "Gitta Klopf (Ansp.) / Ralf Jooß (Obermeister)", "email": "info@elektroinnung-hassberge.de", "phone": "09526 8250"},
"Innung für Elektro- und Informationstechnik Schweinfurt": {"contact": "Gaby Fröschel / Roland Klöffel / Ronald Niessner / Rainer Walter-Helk (Obermeister)", "email": "info@elektroinnung-sw.de", "phone": "09721 41175"},
"Innung für Elektro- und Informationstechnik Würzburg": {"contact": "Heike Langner (Ansp.) / Sebastian Seynstahl (Obermeister)", "email": "mailbox@elektro-innung-wuerzburg.de", "phone": "0931 4501790"},
# Fotografen
"Berufsfotografen-Innung für Unterfranken": {"contact": "Michael Alfen (Obermeister)", "email": "info@foto-alfen.de", "phone": "06021 23807"},
# Friseure
"Friseur-Innung Aschaffenburg Stadt und Land": {"contact": "Corina Bayer (Obermeisterin)", "email": "friseurinnung-aschaffenburg@t-online.de", "phone": "06021 12646"},
"Friseur-Innung Haßberge": {"contact": "Heinz Göhr (Ansp.) / Oliver Merkl (Obermeister)", "email": "info@team-art-of-hair.com", "phone": "09522 7948"},
"Friseurinnung Kitzingen": {"contact": "Sabine Hack (Obermeisterin)", "email": "sabine.hack71@web.de", "phone": "09321 389988"},
"Friseurinnung Miltenberg": {"contact": "Monique Haas (Obermeisterin)", "email": "info@haarmonique.de", "phone": "0160 1405397"},
"Friseurinnung Main-Rhön": {"contact": "Brigitte Rapp (Ansp.) / Margit Rosentritt (Obermeisterin)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Friseur-Innung Würzburg, Main-Spessart und Bad Kissingen": {"contact": "Katharina Walker (Obermeisterin)", "email": "katharinawalker88@gmx.de", "phone": "0931 4605400"},
# Glaser
"Glaserinnung Unterfranken": {"contact": "Siegfried Frank (Obermeister)", "email": "info@frank-bauglaserei.de", "phone": "09321 31890"},
# Kaminkehrer
"Kaminkehrer-Innung Unterfranken": {"contact": "Benjamin Schreck (Obermeister)", "email": "info@kaminkehrerinnung-unterfranken.de", "phone": "09302 2187"},
# Karosserie
"Karosserie- und Fahrzeugtechnik Innung Unterfranken": {"contact": "Manuela Wohlert (Ansp.) / Michael Seidel (Obermeister)", "email": "info@seidel-karosserie.de", "phone": "0911 2358880"},
# Kfz
"Kfz-Innung Unterfranken": {"contact": "Michael Frank (Ansp.) / Roland Hoier (Obermeister)", "email": "info@kfz-innung-ufr.de", "phone": "0931 279910"},
# Landmaschinen
"Innung für Land- und Baumaschinentechnik Unterfranken": {"contact": "Brigitte Rapp (Ansp.) / Bertram Muth (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
# Maler
"Maler-, Tüncher- und Lackierer Innung Alzenau": {"contact": "Karlheinz Trageser (Obermeister)", "email": "maler_trageser@gmx.de", "phone": "06023 8736"},
"Maler- und Lackierer-Innung Aschaffenburg Stadt und Land": {"contact": "Uta Kern (Ansp.) / Ansgar Kern (Obermeister)", "email": "uta.kern@kolb-kern.de", "phone": "06021 859120"},
"Maler- und Lackiererinnung Bad Kissingen": {"contact": "Petra Schwab (Ansp.) / Mathias Stöth (Obermeister)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Maler- und Tüncherinnung Haßberge": {"contact": "Michael Ott (Obermeister)", "email": "obermeister@malerinnung-hassberge.de", "phone": "09534 173330"},
"Maler- und Lackiererinnung Kitzingen": {"contact": "Sandra und Andreas Zobel (Ansp.) / Thomas Wandler (Obermeister)", "email": "info@malerinnung-kitzingen.de", "phone": "09381 9141"},
"Maler- und Lackiererinnung Miltenberg": {"contact": "Melitta Becker (Ansp.) / Jan Becker (Obermeister)", "email": "info@farbe-miltenberg.de", "phone": "09371 1090"},
"Maler-, Tüncher- und Lackierer-Innung Rhön-Grabfeld": {"contact": "Birgit Neuhöfer (Ansp.) / Stefan Neuhöfer (Obermeister)", "email": "info@malerinnung-rg.de", "phone": "09766 1555"},
"Malerinnung Schweinfurt Stadt- und Land": {"contact": "Brigitte Rapp (Ansp.) / Andreas Spath (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Maler- und Stuckateur-Innung Würzburg und Main-Spessart": {"contact": "Claudius Wolfrum (Ansp.) / Peter Killinger (Obermeister)", "email": "info@malerinnung-wuerzburg.de", "phone": "0931 54306"},
# Metall
"Innung Metallbau- und Feinwerktechnik Bayerischer Untermain": {"contact": "Claudia Find / Stefanie Belle (Ansp.) / Matthias Kreß (Obermeister)", "email": "info@innung-metallbau-feinwerktechnik.de", "phone": "06021 401286"},
"Metall-Innung Bad Kissingen/Rhön-Grabfeld": {"contact": "Petra Schwab (Ansp.) / Klaus Engelmann (Obermeister)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Metallinnung Schweinfurt - Haßberge": {"contact": "Brigitte Rapp (Ansp.) / René Dauelsberg (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Metallinnung Mainfranken - Mitte": {"contact": "Birgit Beckmann (Ansp.) / Detlef Lurz (Obermeister)", "email": "info@metallinnung-mainfranken.de", "phone": "0931 412614"},
# Metzger
"Metzgerinnung Aschaffenburg": {"contact": "Dagobert Pfarr (Ansp.) / Marco Häuser (Obermeister)", "email": "metzgerei-pfarr@t-online.de", "phone": "06027 8468"},
"Fleischer-Innung Main-Spessart": {"contact": "Sebastian Bumm (Ansp./komm. Stellv. Obermeister)", "email": "fleischerinnungMSP@gmx.de", "phone": ""},
"Metzgerinnung Miltenberg": {"contact": "Josef Neuberger (Obermeister)", "email": "j.neuberger@t-online.de", "phone": "09371 2671"},
"Metzgerinnung Main-Rhön": {"contact": "Jürgen Straub / Sonja Grob (Ansp.) / Barbara Fink (Obermeisterin)", "email": "innung@fleischerring.de", "phone": "09721 65050"},
"Metzger-Innung Würzburg": {"contact": "Horst Schömig (Obermeister)", "email": "horst@schoemig.eu", "phone": "0931 73926"},
# Ofen
"Unterfränkische Ofen- und Luftheizungsbauer-Innung": {"contact": "Josef Bock (Ansp.) / Michael Heigel (Obermeister)", "email": "josef.bock60@gmail.com", "phone": "0175 8855931"},
# Raumausstatter
"Raumausstatter- und Sattlerinnung Unterfranken": {"contact": "Brigitte Rapp (Ansp.) / Hermann Noske (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
# SHK
"Spengler-, Sanitär- und Heizungstechnik Innung Aschaffenburg - Miltenberg": {"contact": "Michael Bramm (Ansp.) / Christoph Winkler (Obermeister)", "email": "shk-aschaffenburg@t-online.de", "phone": "06021 28731"},
"Innung für Spengler-, Sanitär-, und Heizungstechnik Kitzingen": {"contact": "Christine Keppner-Siegert (Ansp.) / Thomas Lößlein (Obermeister)", "email": "innung-kitzingen@freenet.de", "phone": "09324 978667"},
"SHK-Innung Main-Spessart Innung Sanitär-, Heizungs- und Klimatechnik": {"contact": "Johannes Reber (Obermeister)", "email": "info@shk-main-spessart.de", "phone": "09355 97400"},
"Innung für Spengler-, Sanitär-, Heizungs- und Klimatechnik Schweinfurt - Main-Rhön": {"contact": "Stefan Köppe (Ansp.) / Heinz Schuchbauer (Obermeister)", "email": "info@shk-schweinfurt.de", "phone": "09721 471526"},
"Innung für Sanitär-, Heizungs-, Klempner- und Klimatechnik Würzburg": {"contact": "Sandra Köller (Ansp.) / Werner Rath (Obermeister)", "email": "innung.shk@t-online.de", "phone": "0931 7841878"},
# Schreiner
"Schreinerinnung Aschaffenburg Stadt und Land": {"contact": "Michael Deller (Obermeister)", "email": "info@dellers-werkstatt.de", "phone": "06021 460428"},
"Schreinerinnung Bad Kissingen": {"contact": "Petra Schwab (Ansp.) / Norbert Borst (Obermeister)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Schreinerinnung Miltenberg-Obernburg": {"contact": "Werner Tausch (Obermeister)", "email": "wt@reichert-betten.de", "phone": "09371 97770"},
"Schreinerinnung Rhön-Grabfeld": {"contact": "Michael Werner (Obermeister)", "email": "info@schreiner-rhoen-grabfeld.de", "phone": "09772 9300990"},
"Schreinerinnung Haßberge Schweinfurt": {"contact": "Brigitte Rapp (Ansp.) / Horst Zitterbart (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Schreinerinnung Mainfranken": {"contact": "Ramona Pfenning (Ansp.) / Thomas Heußlein (Obermeister)", "email": "info@schreinerinnung-mainfranken.de", "phone": "09394 9957944"},
# Schuhmacher
"Schuhmacherinnung Unterfranken": {"contact": "Leo Emge (Obermeister)", "email": "l.emge@t-online.de", "phone": "06021 423235"},
# Steinmetz
"Steinmetz- und Steinbildhauerinnung Unterfranken": {"contact": "Brigitte Rapp (Ansp.) / Sebastian Ludwig (Obermeister)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
# Uhrmacher
"Uhrmacher-, Gold- und Silberschmiedeinnung Unterfranken": {"contact": "Markus Graf (Ansp.) / Klaus Imhof (Obermeister)", "email": "m.graf@hwk-ufr.de", "phone": "0931 309081132"},
# Zimmerer
"Zimmerer-Innung Aschaffenburg - Miltenberg": {"contact": "Theresa Breunig (Ansp.) / Jürgen Pfarr (Obermeister)", "email": "info@zimmerer-aschaffenburg-miltenberg.de", "phone": "06094 1361"},
"Zimmerer-Innung Bad Neustadt": {"contact": "Michael Eyrich-Halbig (Obermeister)", "email": "info@holzbau-eyrich.de", "phone": "09736 223"},
"Zimmerer-Innung Main-Spessart": {"contact": "Ulrike Feser (Ansp.) / Volker Schäfer (Obermeister)", "email": "info@zimmerer-main-spessart.de", "phone": "09355 97400"},
"Zimmerer-Innung Schweinfurt-Haßberge": {"contact": "Brigitte Rapp (Ansp.) / Marion Reichhold (Obermeisterin)", "email": "rapp@kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Zimmerer-Innung Würzburg - Kitzingen": {"contact": "Ulrike Feser (Ansp.) / Hermann Lang (Obermeister)", "email": "info@zimmerer-wuerzburg-kitzingen.de", "phone": "0931 72760"},
# Kreishandwerkerschaften
"Kreishandwerkerschaft Aschaffenburg": {"contact": "Claudia Find (Ansp.) / Matthias Kreß (KH-Meister)", "email": "info@khw-ab.de", "phone": "06021 401286"},
"Kreishandwerkerschaft Bad Kissingen": {"contact": "Petra Schwab (Ansp.) / Ulrike Lochner-Erhard (KH-Meisterin)", "email": "khw-kg@t-online.de", "phone": "0971 78536971"},
"Kreishandwerkerschaft Haßberge": {"contact": "Gitta Klopf (Ansp.) / Udo Merz (KH-Meister)", "email": "info@elektroinnung-hassberge.de", "phone": "09524 7492"},
"Kreishandwerkerschaft Kitzingen": {"contact": "Elisabeth Hofmann (Ansp.) / Monika Henneberger (KH-Meisterin)", "email": "khw.kitzingen@gmail.com", "phone": "09332 590636"},
"Kreishandwerkerschaft Main-Spessart": {"contact": "Petra Stegerwald (Ansp.) / Thomas Heußlein (KH-Meister)", "email": "petra@stegerwald.de", "phone": "09352 6056495"},
"Kreishandwerkerschaft Miltenberg": {"contact": "Monique Haas (KH-Meisterin)", "email": "kreishandwerker.mil@gmail.com", "phone": "09371 6693681"},
"Kreishandwerkerschaft Rhön-Grabfeld": {"contact": "Bruno Werner (KH-Meister)", "email": "khwsch-rhoen-grabfeld@mail.de", "phone": "09772 9300990"},
"Kreishandwerkerschaft Schweinfurt": {"contact": "Brigitte Rapp (Ansp.) / Margit Rosentritt (KH-Meisterin)", "email": "rapp@Kreishandwerkerschaft-sw.de", "phone": "09721 473578"},
"Kreishandwerkerschaft Würzburg": {"contact": "Sandra Köller (Ansp.) / Martin Strobl (KH-Meister)", "email": "info@kreishandwerkerschaft-wuerzburg.de", "phone": "0931 7841879"},
}
# Read current leads.csv - only Unterfranken entries
csv_entries = {}
with open(r'C:\Users\a931627\Documents\stadtwerke-saas-analysis\leads\leads.csv', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
if row.get('Region','') == 'Unterfranken':
csv_entries[row['Firm/Innung'].strip()] = {
'contact': row.get('Contact Person',''),
'email': row.get('Email',''),
'phone': row.get('Phone',''),
}
# Compare
print("=== IN PDF BUT NOT IN CSV (missing leads) ===")
missing = []
for name in pdf_entries:
if name not in csv_entries:
# fuzzy check
found = False
for csv_name in csv_entries:
# normalize
if name.lower().replace('','-').replace(' ',' ') in csv_name.lower() or csv_name.lower() in name.lower():
found = True
break
if not found:
missing.append(name)
print(f" MISSING: {name}")
print(f"\nTotal missing: {len(missing)}")
print("\n=== EMAIL DISCREPANCIES (PDF vs CSV) ===")
for pdf_name, pdf_data in pdf_entries.items():
for csv_name, csv_data in csv_entries.items():
# Match by similar name
if (pdf_name.lower()[:20] in csv_name.lower()[:25] or csv_name.lower()[:20] in pdf_name.lower()[:25]):
pdf_email = pdf_data.get('email','').lower().strip()
csv_email = csv_data.get('email','').lower().strip()
if pdf_email and csv_email and pdf_email != csv_email:
print(f" {pdf_name}")
print(f" PDF: {pdf_email}")
print(f" CSV: {csv_email}")
print("\n=== IN CSV BUT DATA NOT MATCHING PDF (spot check) ===")
# Check Steinmetz specifically
for csv_name, csv_data in csv_entries.items():
if 'steinmetz' in csv_name.lower() or 'steinbild' in csv_name.lower():
print(f"CSV: {csv_name} | email: {csv_data['email']} | contact: {csv_data['contact']}")
if 'bäckerinnung kitzingen' in csv_name.lower():
print(f"CSV: {csv_name} | email: {csv_data['email']}")
if 'land- und bau' in csv_name.lower():
print(f"CSV: {csv_name} | email: {csv_data['email']}")

View File

@ -1,75 +1,78 @@
Firm/Innung,Contact Person,Email,Region Firm/Innung,Contact Person,Email,Phone,Region
Augenoptiker-Innung Koeln-Aachen,,info@optikerinnung.de,Köln Augenoptiker-Innung Koeln-Aachen,Hans Josef Schümmer (Obermeister),info@optikerinnung.de,,Köln
Bau-Innung Kreis Mettmann,Thomas Grünendahl,info@handwerk-me.de,Düsseldorf/Surrounding Bau-Innung Kreis Mettmann,"Thomas Grünendahl (KHM) / Simon Taps (GF)",info@handwerk-me.de,,Düsseldorf/Surrounding
Bau-Innung Mönchengladbach,Dipl.-Ing. Frank Bühler,info@kh-mg.de,Düsseldorf/Surrounding Bau-Innung Mönchengladbach,Dipl.-Ing. Frank Bühler,info@kh-mg.de,Düsseldorf/Surrounding
Bau-Innung Neuss-Viersen,Thomas Goldmann,info@kh-niederrhein.de,Düsseldorf/Surrounding Bau-Innung Neuss-Viersen,"Joachim Selzer (KHM) / Thomas Gütgens (GF)",info@kh-niederrhein.de,,Düsseldorf/Surrounding
Bau-Innung Remscheid,Carsten Hof,info@handwerk-remscheid.de,Düsseldorf/Surrounding Bau-Innung Remscheid,Carsten Hof,info@handwerk-remscheid.de,Düsseldorf/Surrounding
Bau-Innung Schweinfurt,Karl Böhner,info@bauinnung-schweinfurt.de,Unterfranken Bau-Innung Schweinfurt,Karl Böhner,info@bauinnung-schweinfurt.de,Unterfranken
Bau-Innung Wuppertal-Solingen,Marcus Koch,info@bauinnung-wuppertal.de,Düsseldorf/Surrounding Bau-Innung Wuppertal-Solingen,"Marcus Koch (Obermeister) / Falk Niederlehner (GF)",info@bauinnung-wuppertal.de,,Düsseldorf/Surrounding
Baugewerks-Innung des Kreises Wesel,Gerhard Landwehrs,info@khwesel.de,Düsseldorf/Surrounding Baugewerks-Innung des Kreises Wesel,Gerhard Landwehrs,info@khwesel.de,Düsseldorf/Surrounding
Baugewerks-Innung Duisburg,Volker Blastik,info@baugewerksinnung-duisburg.de,Düsseldorf/Surrounding Baugewerks-Innung Duisburg,Volker Blastik,info@baugewerksinnung-duisburg.de,Düsseldorf/Surrounding
Bauinnung Aschaffenburg,Felix Englert,felix.englert@bauinnung-aschaffenburg.de,Unterfranken Bauinnung Aschaffenburg,Felix Englert,felix.englert@bauinnung-aschaffenburg.de,Unterfranken
Bauinnung Bad Kissingen / Rhön-Grabfeld,Stefan Goos,stefan.goos@zehe-gmbh.de,Unterfranken Bauinnung Bad Kissingen / Rhön-Grabfeld,Stefan Goos,stefan.goos@zehe-gmbh.de,Unterfranken
Bauinnung Mainfranken - Würzburg,Ralf Stegmeier,info@trend-bau.com,Unterfranken Bauinnung Mainfranken - Würzburg,Manfred Dallner,baugewerbe@lbb-unterfranken.de,Unterfranken
Berufsfotografen-Innung für Unterfranken,,info@foto-alfen.de,Unterfranken Berufsfotografen-Innung für Unterfranken,,info@foto-alfen.de,Unterfranken
Brauer- und Mälzerinnung Unterfranken,,info@private-brauereien-bayern.de,Unterfranken Brauer- und Mälzerinnung Unterfranken,,info@private-brauereien-bayern.de,Unterfranken
"Buechsenmacher-Innung Nordrhein, RLP und Saarland",,kliedl@t-online.de,Köln "Buechsenmacher-Innung Nordrhein, RLP und Saarland",Klaus-Bernd Liedl (Obermeister),kliedl@t-online.de,,Köln
Bundesinnung fuer das Geruestbauer-Handwerk,,info@geruestbauhandwerk.de,Köln Bundesinnung fuer das Geruestbauer-Handwerk,Marcus Nachbauer (Bundesinnungsmeister) / Sabrina Luther (GF),info@geruestbauhandwerk.de,,Köln
Bäckerinnung Bad Kissingen - Rhön-Grabfeld,Petra Schwab,khw-kg@t-online.de,Unterfranken Bäckerinnung Bad Kissingen - Rhön-Grabfeld,Petra Schwab,khw-kg@t-online.de,Unterfranken
Bäckerinnung Bayerischer Untermain,,v.hench@baeckerinnung-bayerischer-untermain.de,Unterfranken Bäckerinnung Bayerischer Untermain,,v.hench@baeckerinnung-bayerischer-untermain.de,Unterfranken
Bäckerinnung Kitzingen,Tilo Brönner,tilo-br@t-online.de,Unterfranken Bäckerinnung Kitzingen,"Elisabeth Hofmann (Ansp.) / Tilo Brönner (Obermeister)",khw.kitzingen@gmail.com,,Unterfranken
Bäckerinnung Mainfranken,Marcel Scherg,scherge.beck@t-online.de,Unterfranken Bäckerinnung Mainfranken,"Christine Winterbauer (Ansp.) / Marcel Scherg (Obermeister)",christine.winterbauer@baeckerbuchstelle.de,,Unterfranken
Bäckerinnung Schweinfurt - Haßberge,Gerhard Götz,baeckerei-goetz@web.de,Unterfranken Bäckerinnung Schweinfurt - Haßberge,Gerhard Götz,baeckerei-goetz@web.de,Unterfranken
Dachdecker- und Zimmerer-Innung Koeln,,e-mail@dachdecker-innung-koeln.de,Köln Dachdecker- und Zimmerer-Innung Koeln,Oliver Miesen (Obermeister) / Bettina Dietrich (GF),e-mail@dachdecker-innung-koeln.de,,Köln
Dachdecker-Innung Duesseldorf,N/A,info@dachdeckerinnung.nrw,0211 / 36 70 710,Düsseldorf
Dachdeckerinnung Aschaffenburg - Miltenberg,,lukas.thalheimer@thalheimer.de,Unterfranken Dachdeckerinnung Aschaffenburg - Miltenberg,,lukas.thalheimer@thalheimer.de,Unterfranken
Dachdeckerinnung Unterfranken,,info@dachdecker-unterfranken.de,Unterfranken Dachdeckerinnung Unterfranken,Timo Markert,info@mein-dachdecker.com,Unterfranken
Drucker- (Buchdrucker-)- und Buchbinder-Innung Duisburg,Bodo H. Oppenberg,info@handwerk-duisburg.de,Düsseldorf/Surrounding Drucker- (Buchdrucker-)- und Buchbinder-Innung Duisburg,"Bodo H. Oppenberg / Lothar Hellmann (KHM) / Michael Dicke (GF)",info@handwerk-duisburg.de,,Düsseldorf/Surrounding
Elektroinnung Koeln,,info@elektroinnungkoeln.de,Köln Elektroinnung Koeln,Etienne Berndt (GF) / Ralf Janowski (Obermeister),info@elektroinnungkoeln.de,0221 123071,Köln
Fleischer-Innung Koeln,,obermeister@fleischer-koeln.de,Köln Fleischer-Innung Koeln,Astrid Schmitz (Obermeisterin),obermeister@fleischer-koeln.de,,Köln
Fleischer-Innung Main-Spessart,Sebastian Bumm,fleischerinnungMSP@gmx.de,Unterfranken Fleischer-Innung Main-Spessart,Sebastian Bumm,fleischerinnungMSP@gmx.de,Unterfranken
Fotografen-Innung Düsseldorf-Aachen-Köln,Guido de Nardo,info@Fotografen-DUS.de,Düsseldorf/Surrounding Fotografen-Innung Düsseldorf-Aachen-Köln,"Guido de Nardo (Vorsitzender) / Torsten Spengler (GF)",info@Fotografen-DUS.de,,Düsseldorf/Surrounding
Friseur-Innung Aschaffenburg Stadt und Land,,friseurinnung-aschaffenburg@t-online.de,Unterfranken Friseur-Innung Aschaffenburg Stadt und Land,,friseurinnung-aschaffenburg@t-online.de,Unterfranken
Friseur-Innung Haßberge,Heinz Göhr,info@team-art-of-hair.com,Unterfranken Friseur-Innung Haßberge,Heinz Göhr,info@team-art-of-hair.com,Unterfranken
Friseur-Innung Koeln,,info@kopfarbeit-koeln.de,Köln Friseur-Innung Koeln,Mike Engels (Obermeister) / Julia Barth (GF),info@kopfarbeit-koeln.de,,Köln
"Friseur-Innung Würzburg, Main-Spessart und Bad Kissingen",,katharinawalker88@gmx.de,Unterfranken "Friseur-Innung Würzburg, Main-Spessart und Bad Kissingen",,katharinawalker88@gmx.de,Unterfranken
Friseurinnung Kitzingen,,sabine.hack71@web.de,Unterfranken Friseurinnung Kitzingen,,sabine.hack71@web.de,Unterfranken
Friseurinnung Main-Rhön,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken Friseurinnung Main-Rhön,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken
Friseurinnung Miltenberg,,info@haarmonique.de,Unterfranken Friseurinnung Miltenberg,,info@haarmonique.de,Unterfranken
Glasapparatebauer-Innung Duisburg,Dieter Verhees,hippler@handwerk-duisburg.de,Düsseldorf/Surrounding Glasapparatebauer-Innung Duisburg,Dieter Verhees,hippler@handwerk-duisburg.de,Düsseldorf/Surrounding
Glaser-Innung Koeln-Bonn-Aachen,,mail@glas-bong.de,Köln Glaser-Innung Koeln-Bonn-Aachen,Anne Bong (Obermeisterin),mail@glas-bong.de,,Köln
Glaserinnung Unterfranken,,info@frank-bauglaserei.de,Unterfranken Glaserinnung Unterfranken,,info@frank-bauglaserei.de,Unterfranken
Graveur- und Metallbildner-Innung Rhein-Ruhr,Till Esser,info@kh-mo.de,Düsseldorf/Surrounding Graveur- und Metallbildner-Innung Rhein-Ruhr,Till Esser,info@kh-mo.de,Düsseldorf/Surrounding
Innung der Metallhandwerke (Solingen),Thomas Blau,info@handwerk-sgw.de,Düsseldorf/Surrounding Innung der Metallhandwerke (Solingen),Thomas Blau,info@handwerk-sgw.de,Düsseldorf/Surrounding
Innung des Bekleidungshandwerks Unterfranken,Friedrun Schlagbauer-Werner,info@schneiderin-wuerzburg.de,Unterfranken Innung des Bekleidungshandwerks Unterfranken,Friedrun Schlagbauer-Werner,info@schneiderin-wuerzburg.de,Unterfranken
Innung des Gebaeudereiniger-Handwerks Koeln-Aachen,,info@gebaeudereiniger-koeln-aachen.de,Köln Innung des Gebaeudereiniger-Handwerks Koeln-Aachen,Detlef Ptak (Obermeister) / Jennifer Schramm (GF),info@gebaeudereiniger-koeln-aachen.de,0221 251064,Köln
Innung des Massschneiderhandwerks Koeln / Textileiniger-Innung Koeln/Bonn,,twp.koeln@gmail.com,Köln Innung des Massschneiderhandwerks Koeln / Textileiniger-Innung Koeln/Bonn,Thomas Wien-Pegelow (Obermeister),twp.koeln@gmail.com,,Köln
Innung Farbe Koeln,,s.epe@epe-maler.de,Köln Innung Farbe Koeln,Sebastian Epe (Obermeister),s.epe@epe-maler.de,,Köln
Innung fuer Informationstechnik Koeln/Bonn/Rhein-Sieg/Rhein-Erft,,n.gassner@koenig-avt.de,Köln Innung fuer Informationstechnik Koeln/Bonn/Rhein-Sieg/Rhein-Erft,Nicolay Gassner (Obermeister),n.gassner@koenig-avt.de,,Köln
Innung fuer Metalltechnik Koeln,,info@van-broek.de,Köln Innung fuer Metalltechnik Koeln,Sascha Franke (Obermeister),info@van-broek.de,,Köln
Innung fuer Orthopaedie-Technik Koeln,,sebastian@malzkorn.at,Köln Innung fuer Orthopaedie-Technik Koeln,Sebastian Malzkorn (Obermeister),sebastian@malzkorn.at,,Köln
Innung für Elektro- und Informationstechnik Bayerischer Untermain,Annett Kinzel,info@elektroinnung-bayerischeruntermain.de,Unterfranken Innung für Elektro- und Informationstechnik Bayerischer Untermain,Annett Kinzel,info@elektroinnung-bayerischeruntermain.de,Unterfranken
Innung für Elektro- und Informationstechnik Haßberge,Gitta Klopf,info@elektroinnung-hassberge.de,Unterfranken Innung für Elektro- und Informationstechnik Haßberge,Gitta Klopf,info@elektroinnung-hassberge.de,Unterfranken
Innung für Elektro- und Informationstechnik Schweinfurt,"Gaby Fröschel, Roland Klöffel, Ronald Niessner",info@elektroinnung-sw.de,Unterfranken Innung für Elektro- und Informationstechnik Schweinfurt,"Gaby Fröschel, Roland Klöffel, Ronald Niessner",info@elektroinnung-sw.de,Unterfranken
Innung für Elektro- und Informationstechnik Würzburg,Heike Langner,mailbox@elektro-innung-wuerzburg.de,Unterfranken Innung für Elektro- und Informationstechnik Würzburg,Heike Langner,mailbox@elektro-innung-wuerzburg.de,Unterfranken
Innung für Land- und Baumaschinentechnik Unterfranken,Brigitte Rapp,info@innung-landbautechnik.de,Unterfranken Innung für Land- und Baumaschinentechnik Unterfranken,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken
Innung für Metallhandwerk des Kreises Kleve,Johannes Flinterhoff,info@kh-kleve.de,Düsseldorf/Surrounding Innung für Metallhandwerk des Kreises Kleve,Johannes Flinterhoff,info@kh-kleve.de,Düsseldorf/Surrounding
Innung für Metallhandwerke Mülheim an der Ruhr-Oberhausen,Johannes Arnzen,info@metallbauinnung-mh-ob.de,Düsseldorf/Surrounding Innung für Metallhandwerke Mülheim an der Ruhr-Oberhausen,"Johannes Arnzen / Barbara Yeboah (GF)",info@metallbauinnung-mh-ob.de,,Düsseldorf/Surrounding
"Innung für Sanitär-, Heizungs-, Klempner- und Klimatechnik Würzburg",Sandra Köller,innung.shk@t-online.de,Unterfranken "Innung für Sanitär-, Heizungs-, Klempner- und Klimatechnik Würzburg",Sandra Köller,innung.shk@t-online.de,Unterfranken
"Innung für Spengler-, Sanitär-, Heizungs- und Klimatechnik Schweinfurt - Main-Rhön",Stefan Köppe,info@shk-schweinfurt.de,Unterfranken "Innung für Spengler-, Sanitär-, Heizungs- und Klimatechnik Schweinfurt - Main-Rhön",Stefan Köppe,info@shk-schweinfurt.de,Unterfranken
"Innung für Spengler-, Sanitär-, und Heizungstechnik Kitzingen",Christine Keppner-Siegert,innung-kitzingen@freenet.de,Unterfranken "Innung für Spengler-, Sanitär-, und Heizungstechnik Kitzingen",Christine Keppner-Siegert,innung-kitzingen@freenet.de,Unterfranken
Innung Koeln Rollladen und Sonnenschutz,,info@rhp-online.de,Köln Innung Koeln Rollladen und Sonnenschutz,Andre Urban (Obermeister),info@rhp-online.de,,Köln
Innung Metallbau- und Feinwerktechnik Bayerischer Untermain,Matthias Kreß,m.kress@wassermannkress.de,Unterfranken Innung des Kraftfahrzeuggewerbes Duesseldorf,Sven Gustavson (GF) / Hermann Görtz (Obermeister),info@kfz-innung-duesseldorf.de,+49 211-6634 34,Düsseldorf
Innung fuer Sanitaer- und Heizungstechnik Duesseldorf,Horst Jansen (GF) / Hans Werner Eschrich (Obermeister),info@shk-duesseldorf.de,0211 36707-10,Düsseldorf
Innung Metallbau- und Feinwerktechnik Bayerischer Untermain,"Claudia Find / Stefanie Belle (Ansp.) / Matthias Kreß (Obermeister)",info@innung-metallbau-feinwerktechnik.de,,Unterfranken
"Juwelier-, Gold- und Silberschmiede-Innung Koeln",,info@sotos-schmuck.de,Köln "Juwelier-, Gold- und Silberschmiede-Innung Koeln",,info@sotos-schmuck.de,Köln
Kaminkehrer-Innung Unterfranken,,info@kaminkehrerinnung-unterfranken.de,Unterfranken Kaminkehrer-Innung Unterfranken,,info@kaminkehrerinnung-unterfranken.de,Unterfranken
Karosserie- und Fahrzeugtechnik Innung Unterfranken,Michael Seidel,info@seidel-karosserie.de,Unterfranken Karosserie- und Fahrzeugtechnik Innung Unterfranken,Michael Seidel,info@seidel-karosserie.de,Unterfranken
Karosseriebauer-Innung Koeln,,info@karosserie-innungkoeln.de,Köln Karosseriebauer-Innung Koeln,Oliver Nienhaus (Obermeister) / Claudia Weiler (GF),info@karosserie-innungkoeln.de,,Köln
Kfz-Innung Unterfranken,Michael Frank,info@kfz-innung-ufr.de,Unterfranken Kfz-Innung Unterfranken,Michael Frank,info@kfz-innung-ufr.de,Unterfranken
Konditoren-Innung Koeln - Bonn,,info@cafe-schoener.de,Köln Konditoren-Innung Koeln - Bonn,Rudolf Schöner (Obermeister),info@cafe-schoener.de,,Köln
Kreishandwerkerschaft Aschaffenburg,Claudia Find,info@khw-ab.de,Unterfranken Kreishandwerkerschaft Aschaffenburg,Claudia Find,info@khw-ab.de,Unterfranken
Kreishandwerkerschaft Bad Kissingen,Petra Schwab,khw-kg@t-online.de,Unterfranken Kreishandwerkerschaft Bad Kissingen,Petra Schwab,khw-kg@t-online.de,Unterfranken
Kreishandwerkerschaft Haßberge,Gitta Klopf,info@elektroinnung-hassberge.de,Unterfranken Kreishandwerkerschaft Haßberge,Gitta Klopf,info@elektroinnung-hassberge.de,Unterfranken
Kreishandwerkerschaft Kitzingen,Monika Henneberger,monika.henneberger@t-online.de,Unterfranken Kreishandwerkerschaft Kitzingen,Monika Henneberger,monika.henneberger@t-online.de,Unterfranken
Kreishandwerkerschaft Koeln,,lepore@handwerk.koeln,Köln Kreishandwerkerschaft Koeln,Roberto Lepore (HGF) / Nicolai Lucks (Kreishandwerksmeister),lepore@handwerk.koeln,,Köln
Kreishandwerkerschaft Main-Spessart,Thomas Heußlein,info@schreinerei-heusslein.de,Unterfranken Kreishandwerkerschaft Main-Spessart,Thomas Heußlein,info@schreinerei-heusslein.de,Unterfranken
Kreishandwerkerschaft Miltenberg,,kreishandwerker.mil@gmail.com,Unterfranken Kreishandwerkerschaft Miltenberg,,kreishandwerker.mil@gmail.com,Unterfranken
Kreishandwerkerschaft Rhön-Grabfeld,,khwsch-rhoen-grabfeld@mail.de,Unterfranken Kreishandwerkerschaft Rhön-Grabfeld,,khwsch-rhoen-grabfeld@mail.de,Unterfranken
@ -83,20 +86,21 @@ Maler- und Lackiererinnung Miltenberg,Melitta Becker,info@farbe-miltenberg.de,Un
Maler- und Stuckateur-Innung Würzburg und Main-Spessart,Claudius Wolfrum,info@malerinnung-wuerzburg.de,Unterfranken Maler- und Stuckateur-Innung Würzburg und Main-Spessart,Claudius Wolfrum,info@malerinnung-wuerzburg.de,Unterfranken
Maler- und Tüncherinnung Haßberge,,obermeister@malerinnung-hassberge.de,Unterfranken Maler- und Tüncherinnung Haßberge,,obermeister@malerinnung-hassberge.de,Unterfranken
"Maler-, Tüncher- und Lackierer-Innung Rhön-Grabfeld",Birgit Neuhöfer,info@malerinnung-rg.de,Unterfranken "Maler-, Tüncher- und Lackierer-Innung Rhön-Grabfeld",Birgit Neuhöfer,info@malerinnung-rg.de,Unterfranken
Malerinnung Schweinfurt Stadt- und Land,Brigitte Rapp,info@malerinnung-schweinfurt.de,Unterfranken Malerinnung Schweinfurt Stadt- und Land,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken
Maßschneider-Innung Düsseldorf,Sandra Gronemeier,info@mass-schneider-innung.de,Düsseldorf/Surrounding Maßschneider-Innung Düsseldorf,Sandra Gronemeier,info@mass-schneider-innung.de,Düsseldorf/Surrounding
Metall-Innung Bad Kissingen/Rhön-Grabfeld,Klaus Engelmann,info@metallinnung-kg-nes.de,Unterfranken Metall-Innung Bad Kissingen/Rhön-Grabfeld,Petra Schwab,khw-kg@t-online.de,Unterfranken
Metall-Innung des Kreises Wesel,Rainer Theunissen,info@metallinnung-wesel.de,Düsseldorf/Surrounding Metall-Innung des Kreises Wesel,Rainer Theunissen,info@metallinnung-wesel.de,Düsseldorf/Surrounding
Metall-Innung Essen,Björn Bergmann,info@metallhandwerk-essen.de,Düsseldorf/Surrounding Metall-Innung Essen,Björn Bergmann,info@metallhandwerk-essen.de,Düsseldorf/Surrounding
Metallinnung Mainfranken - Mitte,Detlef Lurz,detlef.lurz@lurz-metalltec.de,Unterfranken Metallinnung Mainfranken - Mitte,Detlef Lurz,detlef.lurz@lurz-metalltec.de,Unterfranken
Metallinnung Schweinfurt - Haßberge,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken Metallinnung Schweinfurt - Haßberge,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken
Metzger-Innung Würzburg,,horst@schoemig.eu,Unterfranken Metzger-Innung Würzburg,,horst@schoemig.eu,Unterfranken
Metzgerinnung Aschaffenburg,Marco Häuser,marco.haeuser@haeuser-hra.de,Unterfranken Metzgerinnung Aschaffenburg,Marco Häuser,marco.haeuser@haeuser-hra.de,Unterfranken
Metzgerinnung Main-Rhön,Barbara Fink,metzgerei_dros_fladungen@outlook.de,Unterfranken Metzgerinnung Main-Rhön,"Jürgen Straub / Sonja Grob (GF Schweinfurt) / Barbara Fink (Obermeisterin)",innung@fleischerring.de,,Unterfranken
Metzgerinnung Miltenberg,,j.neuberger@t-online.de,Unterfranken Metzgerinnung Miltenberg,,j.neuberger@t-online.de,Unterfranken
Musikinstrumentenmacher-Innung Nordrhein,Christoph Böttcher,info@musikinstrumentenmacher-innung.de,Düsseldorf/Surrounding Musikinstrumentenmacher-Innung Nordrhein,Christoph Böttcher,info@musikinstrumentenmacher-innung.de,Düsseldorf/Surrounding
Raumausstatter- und Sattlerinnung Unterfranken,Hermann Noske,mail@sofa-shop.de,Unterfranken Raumausstatter- und Sattlerinnung Unterfranken,Hermann Noske,mail@sofa-shop.de,Unterfranken
Raumausstatter-Innung Koeln,,info@diana-breidenbach.de,Köln Raumausstatter-Innung Koeln,Diana Goeddertz (Obermeisterin),info@diana-breidenbach.de,,Köln
SHK-Innung Koeln,N/A,info@shk-innung-koeln.de,(0221) 83712-0,Köln
Schreinerinnung Aschaffenburg Stadt und Land,,info@dellers-werkstatt.de,Unterfranken Schreinerinnung Aschaffenburg Stadt und Land,,info@dellers-werkstatt.de,Unterfranken
Schreinerinnung Bad Kissingen,Norbert Borst,khw-kg@t-online.de,Unterfranken Schreinerinnung Bad Kissingen,Norbert Borst,khw-kg@t-online.de,Unterfranken
Schreinerinnung Haßberge Schweinfurt,Horst Zitterbart,schreinerei.zitterbart@t-online.de,Unterfranken Schreinerinnung Haßberge Schweinfurt,Horst Zitterbart,schreinerei.zitterbart@t-online.de,Unterfranken
@ -106,13 +110,13 @@ Schreinerinnung Rhön-Grabfeld,,info@schreiner-rhoen-grabfeld.de,Unterfranken
Schuhmacherinnung Unterfranken,,l.emge@t-online.de,Unterfranken Schuhmacherinnung Unterfranken,,l.emge@t-online.de,Unterfranken
"SHK-Innung Main-Spessart Innung Sanitär-, Heizungs- und Klimatechnik",,info@shk-main-spessart.de,Unterfranken "SHK-Innung Main-Spessart Innung Sanitär-, Heizungs- und Klimatechnik",,info@shk-main-spessart.de,Unterfranken
"Spengler-, Sanitär- und Heizungstechnik Innung Aschaffenburg - Miltenberg",Michael Bramm,shk-aschaffenburg@t-online.de,Unterfranken "Spengler-, Sanitär- und Heizungstechnik Innung Aschaffenburg - Miltenberg",Michael Bramm,shk-aschaffenburg@t-online.de,Unterfranken
Steinmetz- und Steinbildhauerinnung Unterfranken,Josef Hofmann,info@stein-welten.com,Unterfranken Steinmetz- und Steinbildhauerinnung Unterfranken,Brigitte Rapp,rapp@kreishandwerkerschaft-sw.de,Unterfranken
Straßen- und Tiefbauer-Innung Düsseldorf,André Grimmert,info@strassenbauer-innung-duesseldorf.de,Düsseldorf/Surrounding Straßen- und Tiefbauer-Innung Düsseldorf,André Grimmert,info@strassenbauer-innung-duesseldorf.de,Düsseldorf/Surrounding
Stuckateur-Innung Koeln - Ausbau + Fassade,,s.rettig@hhhuerth.de,Köln Stuckateur-Innung Koeln - Ausbau + Fassade,Sarah M. Rettig (Obermeisterin),s.rettig@hhhuerth.de,,Köln
"Uhrmacher-, Gold- und Silberschmiedeinnung Unterfranken",Klaus Imhof,kontakt@juwelier-imhof.de,Unterfranken "Uhrmacher-, Gold- und Silberschmiedeinnung Unterfranken",Klaus Imhof,kontakt@juwelier-imhof.de,Unterfranken
Unterfränkische Ofen- und Luftheizungsbauer-Innung,Michael Heigel,heigel@heigel.de,Unterfranken Unterfränkische Ofen- und Luftheizungsbauer-Innung,Michael Heigel,heigel@heigel.de,Unterfranken
Verband der Berufsfotografen Ruhr,Andreas Köhring,ak@koehring-fotografie.de,Düsseldorf/Surrounding Verband der Berufsfotografen Ruhr,Andreas Köhring,ak@koehring-fotografie.de,Düsseldorf/Surrounding
Werbetechniker-Innung Koeln - Bonn - Aachen,,info@werbetechnik-baecker.de,Köln Werbetechniker-Innung Koeln - Bonn - Aachen,Markus Böcker (Obermeister),info@werbetechnik-baecker.de,,Köln
Zimmerer-Innung Aschaffenburg - Miltenberg,Theresa Breunig,info@zimmerer-aschaffenburg-miltenberg.de,Unterfranken Zimmerer-Innung Aschaffenburg - Miltenberg,Theresa Breunig,info@zimmerer-aschaffenburg-miltenberg.de,Unterfranken
Zimmerer-Innung Bad Neustadt,,info@holzbau-eyrich.de,Unterfranken Zimmerer-Innung Bad Neustadt,,info@holzbau-eyrich.de,Unterfranken
Zimmerer-Innung Main-Spessart,Volker Schäfer,info@schaefer-halsbach.de,Unterfranken Zimmerer-Innung Main-Spessart,Volker Schäfer,info@schaefer-halsbach.de,Unterfranken

Can't render this file because it has a wrong number of fields in line 4.

24
leads/read_pdf.py Normal file
View File

@ -0,0 +1,24 @@
import pdfplumber
all_text = []
with pdfplumber.open(r'C:\Users\a931627\Documents\stadtwerke-saas-analysis\leads\raw\unterfranken.pdf') as pdf:
for i, page in enumerate(pdf.pages):
w = page.width
h = page.height
# Split into left and right columns
left = page.crop((0, 0, w/2, h))
right = page.crop((w/2, 0, w, h))
left_text = left.extract_text(x_tolerance=2, y_tolerance=2) or ""
right_text = right.extract_text(x_tolerance=2, y_tolerance=2) or ""
all_text.append(f"=== PAGE {i+1} LEFT ===\n{left_text}")
all_text.append(f"=== PAGE {i+1} RIGHT ===\n{right_text}")
full = "\n".join(all_text)
# Write to file for easier reading
with open(r'C:\Users\a931627\Documents\stadtwerke-saas-analysis\leads\unterfranken_pdf_raw.txt', 'w', encoding='utf-8') as f:
f.write(full)
print(f"Written {len(full)} chars")
print(full[:3000])

File diff suppressed because it is too large Load Diff