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:
@@ -1,11 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
<div class="min-h-screen bg-gray-50 flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="hidden lg:flex flex-col w-64 bg-white border-r border-gray-100 fixed inset-y-0 z-30">
|
||||
<!-- Logo -->
|
||||
<div class="h-16 flex items-center gap-3 px-6 border-b border-gray-100">
|
||||
<div class="w-8 h-8 rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center shrink-0">
|
||||
<span class="text-white font-bold text-sm">S</span>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900 text-lg">ShiftCraft</span>
|
||||
</div>
|
||||
|
||||
<UMain>
|
||||
<UContainer class="py-8">
|
||||
<!-- Nav links -->
|
||||
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
||||
<NuxtLink
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
|
||||
:class="isActive(item.to) ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'"
|
||||
>
|
||||
<UIcon :name="item.icon" class="w-5 h-5 shrink-0" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Bottom: plan badge + user -->
|
||||
<div class="px-3 py-4 border-t border-gray-100 space-y-3">
|
||||
<div class="px-3 py-2 rounded-xl bg-indigo-50 border border-indigo-100 flex items-center gap-2">
|
||||
<span class="text-indigo-600 text-xs font-semibold uppercase tracking-wide">{{ planLabel }}</span>
|
||||
<span class="ml-auto text-indigo-400">
|
||||
<UIcon v-if="plan !== 'free'" name="i-lucide-zap" class="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<NuxtLink to="/settings/billing" class="block px-3 py-2 rounded-xl text-xs text-gray-500 hover:bg-gray-50 transition-colors" v-if="plan === 'free'">
|
||||
Auf Pro upgraden →
|
||||
</NuxtLink>
|
||||
<div class="flex items-center gap-3 px-3 py-2">
|
||||
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
|
||||
<span class="text-indigo-700 text-sm font-medium">{{ userInitial }}</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">{{ userName }}</p>
|
||||
</div>
|
||||
<UButton icon="i-lucide-log-out" color="neutral" variant="ghost" size="xs" @click="handleSignOut" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile header -->
|
||||
<div class="lg:hidden fixed top-0 inset-x-0 z-30 h-14 bg-white border-b border-gray-100 flex items-center justify-between px-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">S</span>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900">ShiftCraft</span>
|
||||
</div>
|
||||
<UButton icon="i-lucide-menu" color="neutral" variant="ghost" @click="mobileOpen = true" />
|
||||
</div>
|
||||
|
||||
<!-- Mobile drawer -->
|
||||
<USlideover v-model:open="mobileOpen" side="left" class="lg:hidden">
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<div class="h-14 flex items-center gap-3 px-6 border-b border-gray-100">
|
||||
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">S</span>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900">ShiftCraft</span>
|
||||
<UButton class="ml-auto" icon="i-lucide-x" color="neutral" variant="ghost" @click="mobileOpen = false" />
|
||||
</div>
|
||||
<nav class="flex-1 px-3 py-4 space-y-1">
|
||||
<NuxtLink
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all"
|
||||
:class="isActive(item.to) ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50'"
|
||||
@click="mobileOpen = false"
|
||||
>
|
||||
<UIcon :name="item.icon" class="w-5 h-5 shrink-0" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</div>
|
||||
</USlideover>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="flex-1 lg:ml-64 pt-14 lg:pt-0">
|
||||
<div class="p-6 lg:p-8 max-w-7xl mx-auto">
|
||||
<slot />
|
||||
</UContainer>
|
||||
</UMain>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const { signOut, user } = useUser()
|
||||
const orgStore = useOrg()
|
||||
const { plan } = useSubscription()
|
||||
|
||||
const mobileOpen = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: 'i-lucide-layout-dashboard' },
|
||||
{ to: '/employees', label: 'Mitarbeiter', icon: 'i-lucide-users' },
|
||||
{ to: '/constraints', label: 'Bedingungen', icon: 'i-lucide-sliders' },
|
||||
{ to: '/schedules', label: 'Schichtpläne', icon: 'i-lucide-calendar' },
|
||||
{ to: '/settings/organization', label: 'Einstellungen', icon: 'i-lucide-settings' },
|
||||
]
|
||||
|
||||
const planLabel = computed(() => {
|
||||
if (plan.value === 'free') return 'Free Plan'
|
||||
if (plan.value === 'pro') return 'Pro Plan'
|
||||
return 'Business Plan'
|
||||
})
|
||||
|
||||
const userName = computed(() => user?.name || user?.email || 'Nutzer')
|
||||
const userInitial = computed(() => (userName.value || 'N').charAt(0).toUpperCase())
|
||||
|
||||
function isActive(path: string) {
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
signOut()
|
||||
await navigateTo('/login')
|
||||
}
|
||||
|
||||
// Load org on mount
|
||||
onMounted(async () => {
|
||||
const { authStore } = usePocketBase()
|
||||
const orgId = authStore.value.record?.org_id as string | undefined
|
||||
if (orgId && !orgStore.org) {
|
||||
await orgStore.fetchOrg(orgId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user