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:
128
app/pages/settings/organization.vue
Normal file
128
app/pages/settings/organization.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="max-w-2xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Organisationseinstellungen</h1>
|
||||
<p class="text-gray-500 mt-1 text-sm">Verwalten Sie die Grundeinstellungen Ihrer Organisation</p>
|
||||
</div>
|
||||
|
||||
<!-- Settings nav -->
|
||||
<div class="flex gap-1 mb-6 border-b border-gray-100">
|
||||
<NuxtLink
|
||||
v-for="tab in settingsTabs"
|
||||
:key="tab.to"
|
||||
:to="tab.to"
|
||||
class="px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px"
|
||||
:class="$route.path === tab.to
|
||||
? 'border-indigo-600 text-indigo-700'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="bg-white rounded-2xl border border-gray-100 shadow-sm p-8 text-center text-gray-400">
|
||||
<UIcon name="i-lucide-loader-2" class="w-6 h-6 animate-spin mx-auto mb-2" />
|
||||
Lade Einstellungen...
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
|
||||
<div class="space-y-5">
|
||||
<UFormField label="Organisationsname" required>
|
||||
<UInput v-model="form.name" placeholder="Mein Unternehmen GmbH" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Zeitzone">
|
||||
<USelect v-model="form.timezone" :options="timezoneOptions" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Branche">
|
||||
<USelect v-model="form.industry" :options="industryOptions" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<UButton color="primary" :loading="saving" @click="save">
|
||||
Speichern
|
||||
</UButton>
|
||||
<UButton color="neutral" variant="ghost" @click="reset">
|
||||
Zurücksetzen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'default', middleware: 'auth' })
|
||||
|
||||
const toast = useToast()
|
||||
const orgStore = useOrg()
|
||||
const { authStore } = usePocketBase()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
timezone: 'Europe/Berlin',
|
||||
industry: 'retail',
|
||||
})
|
||||
|
||||
const settingsTabs = [
|
||||
{ to: '/settings/organization', label: 'Organisation' },
|
||||
{ to: '/settings/shifts', label: 'Schichten' },
|
||||
{ to: '/settings/billing', label: 'Abonnement' },
|
||||
]
|
||||
|
||||
const timezoneOptions = [
|
||||
{ label: 'Europa/Berlin (CET)', value: 'Europe/Berlin' },
|
||||
{ label: 'Europa/Wien (CET)', value: 'Europe/Vienna' },
|
||||
{ label: 'Europa/Zürich (CET)', value: 'Europe/Zurich' },
|
||||
{ label: 'UTC', value: 'UTC' },
|
||||
]
|
||||
|
||||
const industryOptions = [
|
||||
{ label: 'Einzelhandel', value: 'retail' },
|
||||
{ label: 'Gastronomie', value: 'hospitality' },
|
||||
{ label: 'Gesundheitswesen', value: 'healthcare' },
|
||||
{ label: 'Logistik', value: 'logistics' },
|
||||
{ label: 'Produktion', value: 'manufacturing' },
|
||||
{ label: 'Sonstiges', value: 'other' },
|
||||
]
|
||||
|
||||
function loadFormFromOrg() {
|
||||
if (orgStore.org) {
|
||||
form.name = orgStore.org.name || ''
|
||||
form.timezone = orgStore.org.timezone || 'Europe/Berlin'
|
||||
form.industry = orgStore.org.industry || 'retail'
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
loadFormFromOrg()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await orgStore.updateOrg({
|
||||
name: form.name,
|
||||
timezone: form.timezone,
|
||||
industry: form.industry,
|
||||
})
|
||||
toast.add({ color: 'success', title: 'Einstellungen gespeichert' })
|
||||
} catch (err) {
|
||||
toast.add({ color: 'error', title: 'Fehler beim Speichern', description: String(err) })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const orgId = orgStore.orgId || (authStore.value.record?.org_id as string | undefined)
|
||||
if (orgId && !orgStore.org) await orgStore.fetchOrg(orgId)
|
||||
loadFormFromOrg()
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user