feat: complete ShiftCraft — AI-powered shift scheduling SaaS
Complete implementation including:
- Landing page with hero, features, how-it-works, pricing
- Employee management (CRUD with soft delete)
- AI constraint parser (Anthropic Claude API)
- German labor law templates (ArbZG §3, §5, §9)
- HiGHS ILP solver for optimal fair schedules
- Schedule calendar result view (employee × date grid)
- Shift framework configuration (periods + shifts)
- Subscription tiers: Free / Pro / Business
- PocketBase setup script with collection creation + seed data
- .env.example with all required variables documented
Pages: employees, constraints (list/new/templates), schedules (list/new/[id]),
settings (organization/shifts/billing), dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
107
app/pages/register.vue
Normal file
107
app/pages/register.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Konto erstellen</h2>
|
||||
<p class="text-gray-500 mb-6 text-sm">Starten Sie kostenlos — keine Kreditkarte erforderlich</p>
|
||||
|
||||
<UForm :schema="schema" :state="formState" @submit="onSubmit" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Ihr Name" name="name">
|
||||
<UInput v-model="formState.name" placeholder="Max Mustermann" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Firma / Organisation" name="orgName">
|
||||
<UInput v-model="formState.orgName" placeholder="Mein Betrieb GmbH" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<UFormField label="E-Mail" name="email">
|
||||
<UInput v-model="formState.email" type="email" placeholder="name@firma.de" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Passwort" name="password">
|
||||
<UInput v-model="formState.password" type="password" placeholder="Mindestens 8 Zeichen" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" color="primary" class="w-full justify-center" :loading="loading" size="lg">
|
||||
Kostenlos registrieren
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-500">
|
||||
Bereits ein Konto?
|
||||
<NuxtLink to="/login" class="text-indigo-600 hover:text-indigo-700 font-medium">Anmelden</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
definePageMeta({ layout: 'auth', middleware: 'guest' })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
|
||||
const formState = reactive({
|
||||
name: '',
|
||||
orgName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(2, 'Name muss mindestens 2 Zeichen lang sein'),
|
||||
orgName: z.string().min(2, 'Organisationsname muss mindestens 2 Zeichen lang sein'),
|
||||
email: z.email('Ungültige E-Mail-Adresse'),
|
||||
password: z.string().min(8, 'Passwort muss mindestens 8 Zeichen lang sein'),
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
async function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
loading.value = true
|
||||
try {
|
||||
const { pb } = usePocketBase()
|
||||
|
||||
// Create org first
|
||||
const slugBase = payload.data.orgName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
||||
const slug = `${slugBase}-${Math.random().toString(36).slice(2, 7)}`
|
||||
|
||||
const newOrg = await pb.collection('organizations').create({
|
||||
name: payload.data.orgName,
|
||||
slug,
|
||||
timezone: 'Europe/Berlin',
|
||||
industry: 'general',
|
||||
plan: 'free',
|
||||
plan_employee_limit: 5,
|
||||
plan_history_months: 1,
|
||||
})
|
||||
|
||||
// Create user
|
||||
const newUser = await pb.collection('users').create({
|
||||
name: payload.data.name,
|
||||
email: payload.data.email,
|
||||
password: payload.data.password,
|
||||
passwordConfirm: payload.data.password,
|
||||
org_id: newOrg.id,
|
||||
role: 'owner',
|
||||
emailVisibility: true,
|
||||
})
|
||||
|
||||
// Update org with owner
|
||||
await pb.collection('organizations').update(newOrg.id, { owner: newUser.id })
|
||||
|
||||
// Login
|
||||
await pb.collection('users').authWithPassword(payload.data.email, payload.data.password)
|
||||
const orgStore = useOrg()
|
||||
await orgStore.fetchOrg(newOrg.id)
|
||||
|
||||
toast.add({ color: 'success', title: 'Willkommen bei ShiftCraft!', description: 'Ihr Konto wurde erfolgreich erstellt.' })
|
||||
await navigateTo('/dashboard')
|
||||
} catch (err) {
|
||||
toast.add({ color: 'error', title: 'Registrierung fehlgeschlagen', description: (err as Error).message })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user