Files
shiftcraft/app/pages/settings/organization.vue
Clanker 36e0946ee4 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>
2026-04-18 07:47:31 +02:00

129 lines
3.8 KiB
Vue

<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>