feat: Implement initial with admin and mobile clients, authentication, data models, and lead generation scripts.
This commit is contained in:
parent
c53a71a5f9
commit
5e2d5fb3ae
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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: {},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
Binary file not shown.
|
|
@ -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']}")
|
||||||
|
|
@ -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.
|
|
|
@ -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
Loading…
Reference in New Issue