New Backend changes

This commit is contained in:
2025-08-31 18:28:59 +02:00
parent b0497142ed
commit 6d76acc0bc
26 changed files with 813 additions and 1379 deletions

View File

@@ -48,17 +48,15 @@ defineShortcuts({
}, },
}) })
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const modal = useModal() const modal = useModal()
const dataType = dataStore.dataTypes[type] const dataType = dataStore.dataTypes[type]
const openTab = ref(0) const openTab = ref(0)
const item = ref(JSON.parse(props.item)) const item = ref(JSON.parse(props.item))
console.log(item.value) console.log(item.value)
@@ -156,7 +154,7 @@ const loadOptions = async () => {
} else if(option.option === "units") { } else if(option.option === "units") {
loadedOptions.value[option.option] = (await supabase.from("units").select()).data loadedOptions.value[option.option] = (await supabase.from("units").select()).data
} else { } else {
loadedOptions.value[option.option] = (await useSupabaseSelect(option.option)) loadedOptions.value[option.option] = (await useEntities(option.option).select())
if(dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter){ if(dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter){
loadedOptions.value[option.option] = loadedOptions.value[option.option].filter(i => dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter(i, item)) loadedOptions.value[option.option] = loadedOptions.value[option.option].filter(i => dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter(i, item))
@@ -210,7 +208,7 @@ const createItem = async () => {
if(props.inModal) { if(props.inModal) {
ret = await dataStore.createNewItem(type,item.value,true) ret = await dataStore.createNewItem(type,item.value,true)
} else { } else {
ret = dataStore.createNewItem(type,item.value) ret = await useEntities(type).create(item.value)//dataStore.createNewItem(type,item.value)
} }
emit('returnData', ret) emit('returnData', ret)
@@ -222,7 +220,8 @@ const updateItem = async () => {
if(props.inModal) { if(props.inModal) {
ret = await dataStore.updateItem(type,item.value, oldItem.value,true) ret = await dataStore.updateItem(type,item.value, oldItem.value,true)
} else { } else {
ret = await dataStore.updateItem(type,item.value, oldItem.value) console.log(item.value)
ret = await useEntities(type).update(item.value.id, item.value)//await dataStore.updateItem(type,item.value, oldItem.value)
} }
emit('returnData', ret) emit('returnData', ret)

View File

@@ -4,6 +4,9 @@ import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
import EntityTable from "~/components/EntityTable.vue"; import EntityTable from "~/components/EntityTable.vue";
import EntityListMobile from "~/components/EntityListMobile.vue"; import EntityListMobile from "~/components/EntityListMobile.vue";
const { has } = usePermission()
const props = defineProps({ const props = defineProps({
type: { type: {
required: true, required: true,
@@ -83,23 +86,6 @@ const filteredRows = computed(() => {
}) })
} }
if(!useRole().generalAvailableRights.value[type].showToAllUsers) {
if(useRole().checkRight(`${type}-viewAll`)){
console.log("Right to Show All")
} else if(useRole().checkRight(type)){
console.log("Only Righty to show Own")
console.log(tempItems)
tempItems = tempItems.filter(item => item.profiles.includes(profileStore.activeProfile.id))
} else {
console.log("No Right to Show")
tempItems = []
}
}
return useSearch(searchString.value, tempItems) return useSearch(searchString.value, tempItems)
}) })
</script> </script>
@@ -139,7 +125,7 @@ const filteredRows = computed(() => {
/> />
<UButton <UButton
v-if="platform !== 'mobile' && useRole().checkRight(`${type}-create`)" v-if="platform !== 'mobile' && has(`${type}-create`)/*&& useRole().checkRight(`${type}-create`)*/"
@click="router.push(`/standardEntity/${type}/create`)" @click="router.push(`/standardEntity/${type}/create`)"
class="ml-3" class="ml-3"
>+ {{dataType.labelSingle}}</UButton> >+ {{dataType.labelSingle}}</UButton>

View File

@@ -217,7 +217,7 @@ const onTabChange = (index) => {
/> />
</div> </div>
<EntityShowSubFiles <!--<EntityShowSubFiles
:item="props.item" :item="props.item"
:query-string-data="getAvailableQueryStringData()" :query-string-data="getAvailableQueryStringData()"
v-else-if="tab.label === 'Dateien'" v-else-if="tab.label === 'Dateien'"
@@ -268,7 +268,7 @@ const onTabChange = (index) => {
:top-level-type="type" :top-level-type="type"
v-else v-else
:platform="platform" :platform="platform"
/> />-->
</template> </template>
</UTabs> </UTabs>
<UDashboardPanelContent v-else style="overflow-x: hidden;"> <UDashboardPanelContent v-else style="overflow-x: hidden;">

View File

@@ -27,9 +27,8 @@ const dataType = dataStore.dataTypes[props.topLevelType]
<template> <template>
<UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''"> <UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
<HistoryDisplay <HistoryDisplay
:type="dataType.historyItemHolder" :type="props.topLevelType"
v-if="props v-if="props.item.id"
.item.id"
:element-id="props.item.id" :element-id="props.item.id"
render-headline render-headline
/> />

View File

@@ -14,8 +14,9 @@ const props = defineProps({
default: false default: false
} }
}) })
const profileStore = useProfileStore()
const supabase = useSupabaseClient() const auth = useAuthStore()
const toast = useToast() const toast = useToast()
const showAddHistoryItemModal = ref(false) const showAddHistoryItemModal = ref(false)
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -28,11 +29,12 @@ const setup = async () => {
if(await useCapacitor().getIsPhone()) platform.value = "mobile" if(await useCapacitor().getIsPhone()) platform.value = "mobile"
if(props.type && props.elementId){ if(props.type && props.elementId){
items.value = (await supabase.from("historyitems").select().eq(props.type,props.elementId).order("created_at",{ascending: true})).data || [] //items.value = (await supabase.from("historyitems").select().eq(props.type,props.elementId).order("created_at",{ascending: true})).data || []
} else { items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`)
} /*else {
items.value = (await supabase.from("historyitems").select().order("created_at",{ascending: true})).data || [] items.value = (await supabase.from("historyitems").select().order("created_at",{ascending: true})).data || []
} }*/
} }
setup() setup()
@@ -40,55 +42,24 @@ setup()
const addHistoryItemData = ref({ const addHistoryItemData = ref({
text: "", text: ""
config: {
type: props.type,
id: props.elementId
}
}) })
const addHistoryItem = async () => { const addHistoryItem = async () => {
console.log(addHistoryItemData.value)
addHistoryItemData.value.createdBy = profileStore.activeProfile.id
addHistoryItemData.value[props.type] = props.elementId const res = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`, {
method: "POST",
body: addHistoryItemData.value
})
const {data,error} = await supabase
.from("historyitems")
.insert([{...addHistoryItemData.value, tenant: profileStore.currentTenant}])
.select()
if(error) {
console.log(error)
} else {
if(addHistoryItemData.value.text.includes("@")){
let usernames = [...addHistoryItemData.value.text.matchAll(/@(\S*)/gm)]
const {data:profiles} = await supabase.from("profiles").select("id,username")
let notifications = usernames.map(i => { addHistoryItemData.value = {}
let rawUsername = i[1] toast.add({title: "Eintrag erfolgreich erstellt"})
showAddHistoryItemModal.value = false
await setup()
return {
tenant: profileStore.currentTenant,
profile: profiles.find(x => x.username === rawUsername).id,
initiatingProfile: profileStore.activeProfile.id,
title: "Sie wurden im Logbuch erwähnt",
link: `/${props.type}s/show/${props.elementId}`,
message: addHistoryItemData.value.text
}
})
console.log(notifications)
const {error} = await supabase.from("notifications").insert(notifications)
}
addHistoryItemData.value = {}
toast.add({title: "Eintrag erfolgreich erstellt"})
showAddHistoryItemModal.value = false
await setup()
}
} }
@@ -170,15 +141,15 @@ const renderText = (text) => {
/> />
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<UAvatar <UAvatar
v-if="!item.createdBy" v-if="!item.created_by"
:src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' " :src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' "
/> />
<UAvatar <UAvatar
:alt="profileStore.getProfileById(item.createdBy).fullName" :alt="item.created_by_profile?.full_name"
v-else v-else
/> />
<div> <div>
<h3 v-if="item.createdBy">{{profileStore.getProfileById(item.createdBy) ? profileStore.getProfileById(item.createdBy).fullName : ""}}</h3> <h3 v-if="item.created_by">{{item.created_by_profile?.full_name}}</h3>
<h3 v-else>FEDEO Bot</h3> <h3 v-else>FEDEO Bot</h3>
<span v-html="renderText(item.text)"/><br> <span v-html="renderText(item.text)"/><br>
<span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span> <span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span>

View File

@@ -1,19 +1,25 @@
<script setup> <script setup>
import {useRole} from "~/composables/useRole.js";
const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const auth = useAuthStore()
const {has} = usePermission()
const role = useRole()
const links = computed(() => { const links = computed(() => {
return [ return [
... profileStore.currentTenant === 21 ? [{ ...auth.profile.pinned_on_navigation.map(pin => {
label: "Lieferschein Formular", if(pin.type === "external") {
to: "https://loar.ma-agrarservice.de/erfassung", return {
icon: "i-heroicons-rectangle-stack", label: pin.label,
target: "_blank", to: pin.link,
}] : [], icon: pin.icon,
... profileStore.currentTenant === 5 ? [{ target: "_blank",
pinned: true
}
}
}),
... false ? [{
label: "Support Tickets", label: "Support Tickets",
to: "/support", to: "/support",
icon: "i-heroicons-rectangle-stack", icon: "i-heroicons-rectangle-stack",
@@ -34,22 +40,22 @@ const links = computed(() => {
icon: "i-heroicons-rectangle-stack", icon: "i-heroicons-rectangle-stack",
defaultOpen: false, defaultOpen: false,
children: [ children: [
... role.checkRight("tasks") ? [{ ... has("tasks") ? [{
label: "Aufgaben", label: "Aufgaben",
to: "/standardEntity/tasks", to: "/standardEntity/tasks",
icon: "i-heroicons-rectangle-stack" icon: "i-heroicons-rectangle-stack"
}] : [], }] : [],
... profileStore.ownTenant.features.planningBoard ? [{ ... true ? [{
label: "Plantafel", label: "Plantafel",
to: "/calendar/timeline", to: "/calendar/timeline",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
}] : [], }] : [],
... profileStore.ownTenant.features.calendar ? [{ ... true ? [{
label: "Kalender", label: "Kalender",
to: "/calendar/grid", to: "/calendar/grid",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
}] : [], }] : [],
... profileStore.ownTenant.features.calendar ? [{ ... true ? [{
label: "Termine", label: "Termine",
to: "/standardEntity/events", to: "/standardEntity/events",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
@@ -97,22 +103,22 @@ const links = computed(() => {
} }
] ]
}, },
... (role.checkRight("customers") || role.checkRight("vendors") || role.checkRight("contacts")) ? [{ ... (has("customers") || has("vendors") || has("contacts")) ? [{
label: "Kontakte", label: "Kontakte",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... role.checkRight("customers") ? [{ ... has("customers") ? [{
label: "Kunden", label: "Kunden",
to: "/standardEntity/customers", to: "/standardEntity/customers",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}] : [], }] : [],
... role.checkRight("vendors") ? [{ ... has("vendors") ? [{
label: "Lieferanten", label: "Lieferanten",
to: "/standardEntity/vendors", to: "/standardEntity/vendors",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... role.checkRight("contacts") ? [{ ... has("contacts") ? [{
label: "Ansprechpartner", label: "Ansprechpartner",
to: "/standardEntity/contacts", to: "/standardEntity/contacts",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
@@ -124,17 +130,17 @@ const links = computed(() => {
defaultOpen:false, defaultOpen:false,
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... profileStore.ownTenant.features.timeTracking ? [{ ... true ? [{
label: "Projektzeiten", label: "Projektzeiten",
to: "/times", to: "/times",
icon: "i-heroicons-clock" icon: "i-heroicons-clock"
}] : [], }] : [],
... profileStore.ownTenant.features.workingTimeTracking ? [{ ... true ? [{
label: "Anwesenheiten", label: "Anwesenheiten",
to: "/workingtimes", to: "/workingtimes",
icon: "i-heroicons-clock" icon: "i-heroicons-clock"
}] : [], }] : [],
... role.checkRight("absencerequests") ? [{ ... has("absencerequests") ? [{
label: "Abwesenheiten", label: "Abwesenheiten",
to: "/standardEntity/absencerequests", to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
@@ -146,7 +152,7 @@ const links = computed(() => {
}, },
] ]
}, },
... profileStore.ownTenant.features.accounting ? [{ ... true ? [{
label: "Buchhaltung", label: "Buchhaltung",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-chart-bar-square", icon: "i-heroicons-chart-bar-square",
@@ -183,7 +189,7 @@ const links = computed(() => {
}, },
] ]
},] : [], },] : [],
... role.checkRight("inventory") ? [{ ... has("inventory") ? [{
label: "Lager", label: "Lager",
icon: "i-heroicons-puzzle-piece", icon: "i-heroicons-puzzle-piece",
defaultOpen: false, defaultOpen: false,
@@ -197,7 +203,7 @@ const links = computed(() => {
to: "/inventory/stocks", to: "/inventory/stocks",
icon: "i-heroicons-square-3-stack-3d" icon: "i-heroicons-square-3-stack-3d"
},*/ },*/
... role.checkRight("spaces") ? [{ ... has("spaces") ? [{
label: "Lagerplätze", label: "Lagerplätze",
to: "/standardEntity/spaces", to: "/standardEntity/spaces",
icon: "i-heroicons-square-3-stack-3d" icon: "i-heroicons-square-3-stack-3d"
@@ -209,22 +215,22 @@ const links = computed(() => {
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-clipboard-document", icon: "i-heroicons-clipboard-document",
children: [ children: [
... role.checkRight("products") ? [{ ... has("products") ? [{
label: "Artikel", label: "Artikel",
to: "/standardEntity/products", to: "/standardEntity/products",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("productcategories") ? [{ ... has("productcategories") ? [{
label: "Artikelkategorien", label: "Artikelkategorien",
to: "/standardEntity/productcategories", to: "/standardEntity/productcategories",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("services") ? [{ ... has("services") ? [{
label: "Leistungen", label: "Leistungen",
to: "/standardEntity/services", to: "/standardEntity/services",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
}] : [], }] : [],
... role.checkRight("servicecategories") ? [{ ... has("servicecategories") ? [{
label: "Leistungskategorien", label: "Leistungskategorien",
to: "/standardEntity/servicecategories", to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
@@ -239,39 +245,39 @@ const links = computed(() => {
to: "/standardEntity/hourrates", to: "/standardEntity/hourrates",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}, },
... role.checkRight("vehicles") ? [{ ... has("vehicles") ? [{
label: "Fahrzeuge", label: "Fahrzeuge",
to: "/standardEntity/vehicles", to: "/standardEntity/vehicles",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... role.checkRight("inventoryitems") ? [{ ... has("inventoryitems") ? [{
label: "Inventar", label: "Inventar",
to: "/standardEntity/inventoryitems", to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("inventoryitems") ? [{ ... has("inventoryitems") ? [{
label: "Inventargruppen", label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups", to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
] ]
}, },
... role.checkRight("checks") ? [{ ... has("checks") ? [{
label: "Überprüfungen", label: "Überprüfungen",
to: "/standardEntity/checks", to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass" icon: "i-heroicons-magnifying-glass"
},] : [], },] : [],
... role.checkRight("projects") ? [{ ... has("projects") ? [{
label: "Projekte", label: "Projekte",
to: "/standardEntity/projects", to: "/standardEntity/projects",
icon: "i-heroicons-clipboard-document-check" icon: "i-heroicons-clipboard-document-check"
},] : [], },] : [],
... role.checkRight("contracts") ? [{ ... has("contracts") ? [{
label: "Verträge", label: "Verträge",
to: "/standardEntity/contracts", to: "/standardEntity/contracts",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
}] : [], }] : [],
... role.checkRight("plants") ? [{ ... has("plants") ? [{
label: "Objekte", label: "Objekte",
to: "/standardEntity/plants", to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
@@ -331,8 +337,49 @@ const links = computed(() => {
</script> </script>
<template> <template>
<UAccordion
:items="links"
:multiple="false"
>
<template #default="{ item, index, open }">
<UButton
:variant="item.pinned ? 'ghost' : 'ghost'"
:color="(item.to && route.path === item.to) || (item.children?.some(c => route.path.includes(c.to))) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
:icon="item.icon"
class="w-full"
:to="item.to"
:target="item.target"
>
{{ item.label }}
<div <template v-if="item.children" #trailing>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']"
/>
</template>
</UButton>
</template>
<template #item="{ item }">
<div class="flex flex-col">
<UButton
v-for="child in item.children"
:key="child.label"
variant="ghost"
:color="child.to === route.path ? 'primary' : 'gray'"
:icon="child.icon"
class="ml-4"
:to="child.to"
:target="child.target"
>
{{ child.label }}
</UButton>
</div>
</template>
</UAccordion>
<!-- <div
v-for="item in links" v-for="item in links"
> >
<UAccordion <UAccordion
@@ -382,5 +429,5 @@ const links = computed(() => {
> >
{{item.label}} {{item.label}}
</UButton> </UButton>
</div> </div>-->
</template> </template>

View File

@@ -1,29 +0,0 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const selectedProfile = ref(profileStore.activeProfile.id)
</script>
<template>
<USelectMenu
:options="profileStore.ownProfiles"
value-attribute="id"
class="w-40"
@change="profileStore.changeProfile(selectedProfile)"
v-model="selectedProfile"
>
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full">
<UAvatar :alt="profileStore.tenants.find(i => profileStore.profiles.find(i => i.id === selectedProfile).tenant === i.id).name" size="md" />
<span class="truncate text-gray-900 dark:text-white font-semibold">{{profileStore.tenants.find(i => profileStore.profiles.find(i => i.id === selectedProfile).tenant === i.id).name}}</span>
</UButton>
<template #option="{option}">
{{profileStore.tenants.find(i => i.id === option.tenant).name}}
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,27 @@
<script setup>
const auth = useAuthStore()
const selectedTenant = ref(auth.user.tenant_id)
</script>
<template>
<USelectMenu
:options="auth.tenants"
value-attribute="id"
class="w-40"
@change="auth.switchTenant(selectedTenant)"
v-model="selectedTenant"
>
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full">
<UAvatar :alt="auth.tenants.find(i => auth.activeTenant === i.id).name" size="md" />
<span class="truncate text-gray-900 dark:text-white font-semibold">{{auth.tenants.find(i => auth.activeTenant === i.id).name}}</span>
</UButton>
<template #option="{option}">
{{option.name}}
</template>
</USelectMenu>
</template>

View File

@@ -1,4 +1,5 @@
<script setup> <script setup>
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState() const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts() const { metaSymbol } = useShortcuts()
@@ -7,23 +8,26 @@ const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const auth = useAuthStore()
const items = computed(() => [ const items = computed(() => [
[{ [{
slot: 'account', slot: 'account',
label: '', label: '',
disabled: true disabled: true
}], [{ }], [/*{
label: 'Mein Profil', label: 'Mein Profil',
icon: 'i-heroicons-user', icon: 'i-heroicons-user',
to: `/profiles/show/${profileStore.activeProfile.id}` to: `/profiles/show/${profileStore.activeProfile.id}`
},*/{
label: 'Passwort ändern',
icon: 'i-heroicons-shield-check',
to: `/password-change`
},{ },{
label: 'Abmelden', label: 'Abmelden',
icon: 'i-heroicons-arrow-left-on-rectangle', icon: 'i-heroicons-arrow-left-on-rectangle',
click: async () => { click: async () => {
await supabase.auth.signOut() await auth.logout()
//await dataStore.clearStore()
await router.push('/login')
} }
}] }]
@@ -33,10 +37,10 @@ const items = computed(() => [
<template> <template>
<UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full"> <UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full">
<template #default="{ open }"> <template #default="{ open }">
<UButton color="gray" variant="ghost" class="w-full" :label="profileStore.activeProfile.fullName" :class="[open && 'bg-gray-50 dark:bg-gray-800']"> <UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<template #leading> <!-- <template #leading>
<UAvatar :alt="profileStore.activeProfile ? profileStore.activeProfile.fullName : ''" size="xs" /> <UAvatar :alt="auth.user.email" size="xs" />
</template> </template>-->
<template #trailing> <template #trailing>
<UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" /> <UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" />
@@ -50,7 +54,7 @@ const items = computed(() => [
Angemeldet als Angemeldet als
</p> </p>
<p class="truncate font-medium text-gray-900 dark:text-white"> <p class="truncate font-medium text-gray-900 dark:text-white">
{{profileStore.activeProfile.email}} {{auth.user.email}}
</p> </p>
</div> </div>
</template> </template>

122
composables/useEntities.ts Normal file
View File

@@ -0,0 +1,122 @@
import { useDataStore } from "~/stores/data"
export const useEntities = (
relation: string,
) => {
const dataStore = useDataStore()
const toast = useToast()
const router = useRouter()
const dataType = dataStore.dataTypes[relation]
const select = async (
select: string = "*",
sortColumn: string | null = null,
ascending: boolean = true,
noArchivedFiltering: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}`, {
method: "GET",
params: {
select,
sort: sortColumn || undefined,
asc: ascending ? "true" : "false"
}
})
let data = res || []
if (dataType && dataType.isArchivable && !noArchivedFiltering) {
data = data.filter((i: any) => !i.archived)
}
return data
}
const selectSingle = async (
idToEq: string | number,
select: string = "*",
withInformation: boolean = false
) => {
if (!idToEq) return null
const res = await useNuxtApp().$api(`/api/resource/${relation}/${idToEq}/${withInformation}`, {
method: "GET",
params: { select }
})
return res
}
const create = async (
payload: Record<string, any>,
noRedirect: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}`, {
method: "POST",
body: payload
})
toast.add({title: `${dataType.labelSingle} hinzugefügt`})
if(dataType.redirect && !noRedirect) {
if(dataType.isStandardEntity) {
await router.push(dataType.redirectToList ? `/standardEntity/${relation}` : `/standardEntity/${relation}/show/${res.id}`)
} else {
await router.push(dataType.redirectToList ? `/${relation}` : `/${relation}/show/${res.id}`)
}
}
//modal.close() TODO: Modal Close wenn in Modal
return res
}
const update = async (
id: string | number,
payload: Record<string, any>,
noRedirect: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}/${id}`, {
method: "PUT",
body: payload
})
toast.add({title: `${dataType.labelSingle} geändert`})
if(dataType.redirect && !noRedirect) {
if(dataType.isStandardEntity) {
await router.push(dataType.redirectToList ? `/standardEntity/${relation}` : `/standardEntity/${relation}/show/${res.id}`)
} else {
await router.push(dataType.redirectToList ? `/${relation}` : `/${relation}/show/${res.id}`)
}
}
//modal.close() TODO: Modal Close wenn in Modal
return res
}
/**
* Soft Delete = archived = true
*/
const archive = async (
id: string | number
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}/${id}`, {
method: "PUT",
body: { archived: true }
})
return res
}
return {select, create, update, archive, selectSingle}
}

View File

@@ -0,0 +1,9 @@
export function usePermission() {
const auth = useAuthStore()
const has = (key: string) => {
return auth.hasPermission(key)
}
return { has }
}

28
composables/useUsers.ts Normal file
View File

@@ -0,0 +1,28 @@
import { useDataStore } from "~/stores/data"
export const useUsers = (
id: string | number,
) => {
const dataStore = useDataStore()
const toast = useToast()
const router = useRouter()
const getProfile = async () => {
const res = await useNuxtApp().$api(`/api/profiles/${id}`, {
method: "GET"
})
return res
}
return {getProfile}
}

View File

@@ -3,19 +3,19 @@
import MainNav from "~/components/MainNav.vue"; import MainNav from "~/components/MainNav.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "../composables/useCapacitor.js"; import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue"; import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
profileStore.initializeData((await supabase.auth.getUser()).data.user.id) const auth = useAuthStore()
//profileStore.initializeData((await supabase.auth.getUser()).data.user.id)
const month = dayjs().format("MM") const month = dayjs().format("MM")
@@ -65,8 +65,8 @@ const actions = [
] ]
const groups = computed(() => const groups = computed(() => [
[{ {
key: 'actions', key: 'actions',
commands: actions commands: actions
},{ },{
@@ -99,7 +99,8 @@ const groups = computed(() =>
commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}}) commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}})
} }
].filter(Boolean)) ].filter(Boolean))
const footerLinks = [/*{ const footerLinks = [
/*{
label: 'Invite people', label: 'Invite people',
icon: 'i-heroicons-plus', icon: 'i-heroicons-plus',
to: '/settings/members' to: '/settings/members'
@@ -112,56 +113,163 @@ const footerLinks = [/*{
</script> </script>
<template> <template>
<UDashboardLayout class="safearea" v-if="profileStore.loaded"> <div v-if="!auth.loading">
<div v-if="auth.tenants.find(t => t.id === auth.activeTenant).locked === 'maintenance_tenant'">
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" :class="['!border-transparent']" :ui="{ left: 'flex-1' }"> <UCard class="max-w-lg text-center p-10">
<template #left> <UColorModeImage
<ProfileDropdown class="w-full" /> light="/Logo_Hell_Weihnachten.png"
</template> dark="/Logo_Dunkel_Weihnachten.png"
</UDashboardNavbar> class=" mx-auto my-10"
v-if="month === '12'"
<UDashboardSidebar id="sidebar"> />
<template #header> <UColorModeImage
<UDashboardSearchButton label="Suche..."/> light="/Logo.png"
</template> dark="/Logo_Dark.png"
class="mx-auto my-10"
<GlobalMessages/> v-else
/>
<MainNav/> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
<div class="flex-1" />
<template #footer>
<div class="flex flex-col w-full">
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div> </div>
</template> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</UDashboardSidebar> Wartungsarbeiten
</UDashboardPanel> </h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Dieser FEDEO Mandant wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut oder verwende einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UDashboardPage>
<UDashboardPanel grow> </UCard>
<slot /> </UContainer>
</div>
<div v-else-if="auth.tenants.find(t => t.id === auth.activeTenant).locked === 'maintenance'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
</p>
</UCard>
</UContainer>
</div>
<div v-else-if="auth.tenants.find(t => t.id === auth.activeTenant).locked === 'no_subscription'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Kein Aktives Abonnement für diesen Mandant.
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Bitte wenden Sie sich an den FEDEO Support um ein Abonnement zu erhalten oder verwenden Sie einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
</UCard>
</UContainer>
</div>
<UDashboardLayout class="safearea" v-else >
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible>
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" :class="['!border-transparent']" :ui="{ left: 'flex-1' }">
<template #left>
<TenantDropdown class="w-full" />
</template>
</UDashboardNavbar>
<UDashboardSidebar id="sidebar">
<!-- <template #header>
<UDashboardSearchButton label="Suche..."/>
</template>-->
<!--
<GlobalMessages/>
-->
<MainNav/>
<div class="flex-1" />
<template #footer>
<div class="flex flex-col w-full">
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div>
</template>
</UDashboardSidebar>
</UDashboardPanel> </UDashboardPanel>
</UDashboardPage>
<UDashboardPage>
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<!-- ~/components/HelpSlideover.vue --> <HelpSlideover/>
<HelpSlideover/>
<!-- ~/components/NotificationsSlideover.vue --> <!--<NotificationsSlideover />
<NotificationsSlideover />
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>-->
</UDashboardLayout>
</div>
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>
</UDashboardLayout>
<div <div
v-else v-else
class="flex flex-col" class="flex flex-col"
@@ -178,12 +286,25 @@ const footerLinks = [/*{
class="w-1/3 mx-auto my-10" class="w-1/3 mx-auto my-10"
v-else v-else
/> />
<div v-if="profileStore.showProfileSelection">
<ProfileSelection/> <div v-if="!auth.activeTenant" class="w-1/2 mx-auto text-center">
<!-- Tenant Selection -->
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. Bitte wählen Sie ein Mandant.</h3>
<div class="mx-auto w-1/2 flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UButton
variant="outline"
color="rose"
@click="auth.logout()"
>Abmelden</UButton>
</div> </div>
<div v-else> <div v-else>
<UProgress animation="carousel" class="w-3/4 mx-auto mt-10" /> <UProgress animation="carousel" class="w-3/4 mx-auto mt-10" />
</div> </div>
@@ -192,7 +313,5 @@ const footerLinks = [/*{
</template> </template>
<style scoped> <style scoped>
.testclass {
color: red;
}
</style> </style>

17
middleware/auth.global.ts Normal file
View File

@@ -0,0 +1,17 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useAuthStore()
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
if (!auth.user && !["/login", "/password-reset"].includes(to.path)) {
return navigateTo("/login")
}
// Wenn eingeloggt → von /login auf /dashboard umleiten
if (auth.user && !auth.user?.must_change_password && to.path === "/login") {
return navigateTo("/")
} else if(auth.user && auth.user.must_change_password && to.path !== "/password-change") {
return navigateTo("/password-change")
}
})

View File

@@ -1,8 +0,0 @@
export default defineNuxtRouteMiddleware((to, _from) => {
const user = useSupabaseUser()
const router = useRouter()
if (!user.value) {
//useCookie('redirect', { path: '/' }).value = to.fullPath
return router.push("/login")
}
})

View File

@@ -1,9 +0,0 @@
export default defineNuxtRouteMiddleware(async (to, _from) => {
const router = useRouter()
console.log(await useCapacitor().getIsPhone())
if(await useCapacitor().getIsPhone()) {
return router.push('/mobile')
}
})

View File

@@ -34,7 +34,8 @@ export default defineNuxtConfig({
supabase: { supabase: {
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo", key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
url: "https://uwppvcxflrcsibuzsbil.supabase.co" url: "https://uwppvcxflrcsibuzsbil.supabase.co",
redirect:false
}, },
vite: { vite: {

View File

@@ -1,5 +1,5 @@
<template> <template>
<UDashboardNavbar title="Home"> <!-- <UDashboardNavbar title="Home">
<template #right> <template #right>
<UTooltip text="Notifications" :shortcuts="['N']"> <UTooltip text="Notifications" :shortcuts="['N']">
<UButton color="gray" variant="ghost" square @click="isNotificationsSlideoverOpen = true"> <UButton color="gray" variant="ghost" square @click="isNotificationsSlideoverOpen = true">
@@ -59,16 +59,14 @@
<display-open-tasks/> <display-open-tasks/>
</UDashboardCard> </UDashboardCard>
</UPageGrid> </UPageGrid>
</UDashboardPanelContent> </UDashboardPanelContent>-->
</template> </template>
<script setup> <script setup>
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue"; import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
definePageMeta({
middleware: ["auth","redirect-to-mobile-index"]
})
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -76,24 +74,14 @@ const toast = useToast()
const router = useRouter() const router = useRouter()
const { isNotificationsSlideoverOpen } = useDashboard() const { isNotificationsSlideoverOpen } = useDashboard()
const items = [[{
label: 'Aufgabe',
icon: 'i-heroicons-paper-airplane',
to: '/tasks/create'
}, {
label: 'Kunde',
icon: 'i-heroicons-user-plus',
to: '/customers/create'
}]]
const supabase = useSupabaseClient()
const user = useSupabaseUser() const user = useSupabaseUser()
const unreadMessages = ref(false)
const setup = async () => { const setup = async () => {
unreadMessages.value = (await supabase.from("notifications").select("id,read").eq("read",false)).data.length > 0
} }
setup() setup()

View File

@@ -1,118 +1,32 @@
<script setup > <script setup lang="ts">
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "~/composables/useCapacitor.js";
definePageMeta({ definePageMeta({
layout: "notLoggedIn" layout: "notLoggedIn"
}) })
const supabase = useSupabaseClient() const auth = useAuthStore()
const user = useSupabaseUser()
const router = useRouter()
const colorMode = useColorMode()
const toast = useToast() const toast = useToast()
const profileStore = useProfileStore()
const isLight = computed({
get () {
return colorMode.value !== 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const email = ref("")
const password = ref("")
const fields = [{
name: 'email',
type: 'text',
label: 'Email',
placeholder: 'E-Mail Adresse'
}, {
name: 'password',
label: 'Password',
type: 'password',
placeholder: 'Passwort'
}]
const authenticateWithAzure = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'azure',
options: {
scopes: 'email',
},
})
console.log(data)
console.log(error)
}
const onSubmit = async (data) => {
const {error, data:{ user}} = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password
})
if(error) {
if(error.toString().toLowerCase().includes("invalid")){
toast.add({title:"Zugangsdaten falsch",color:"rose"})
}
} else {
//console.log("Login Successful")
profileStore.initializeData(user.id)
if(await useCapacitor().getIsPhone()) {
router.push("/mobile")
} else {
router.push("/")
}
const doLogin = async (data:any) => {
try {
await auth.login(data.email, data.password)
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Einloggen erfolgreich"})
return navigateTo("/")
} catch (err: any) {
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
} }
} }
</script> </script>
<template> <template>
<!-- <div id="loginSite">
<div id="loginForm">
<UFormGroup
label="E-Mail:"
>
<UInput
v-model="email"
/>
</UFormGroup>
<UFormGroup
label="Passwort:"
>
<UInput
v-model="password"
type="password"
@keyup.enter="onSubmit"
/>
</UFormGroup>
<UButton
@click="onSubmit"
class="mt-3"
>
Einloggen
</UButton>
</div>
</div>-->
<UCard class="max-w-sm w-full mx-auto mt-5"> <UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage <UColorModeImage
light="/Logo.png" light="/Logo.png"
dark="/Logo_Dark.png" dark="/Logo_Dark.png"
/> />
<UAuthForm <UAuthForm
@@ -131,29 +45,13 @@ const onSubmit = async (data) => {
placeholder: 'Dein Passwort' placeholder: 'Dein Passwort'
}]" }]"
:loading="false" :loading="false"
@submit="onSubmit" @submit="doLogin"
:providers="[{label: 'MS365',icon: 'i-simple-icons-microsoft',color: 'gray',click: authenticateWithAzure}]"
:submit-button="{label: 'Weiter'}" :submit-button="{label: 'Weiter'}"
divider="oder" divider="oder"
> >
<template #password-hint>
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
</template>
</UAuthForm> </UAuthForm>
</UCard> </UCard>
</template>
</template>
<style scoped>
#loginSite {
display: flex;
align-content: center;
justify-content: center;
}
#loginForm {
width: 30vw;
height: 30vh;
}
</style>

62
pages/password-change.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
definePageMeta({
layout: "notLoggedIn"
})
const auth = useAuthStore()
const toast = useToast()
const doChange = async (data:any) => {
try {
const res = await useNuxtApp().$api("/api/auth/password/change", {
method: "POST",
body: {
old_password: data.oldPassword,
new_password: data.newPassword,
}
})
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Ändern erfolgreich"})
await auth.logout()
return navigateTo("/login")
} catch (err: any) {
toast.add({title:"Es gab ein Problem beim ändern",color:"rose"})
}
}
</script>
<template>
<UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAuthForm
title="Passwort zurücksetzen"
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
align="bottom"
:fields="[{
name: 'oldPassword',
label: 'Altes Passwort',
type: 'password',
placeholder: 'Dein altes Passwort'
},{
name: 'newPassword',
label: 'Neues Passwort',
type: 'password',
placeholder: 'Dein neues Passwort'
}]"
:loading="false"
@submit="doChange"
:submit-button="{label: 'Ändern'}"
divider="oder"
>
</UAuthForm>
</UCard>
</template>

55
pages/password-reset.vue Normal file
View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
definePageMeta({
layout: "notLoggedIn"
})
const auth = useAuthStore()
const toast = useToast()
const doReset = async (data:any) => {
try {
const res = await useNuxtApp().$api("/auth/password/reset", {
method: "POST",
body: {
email: data.email
}
})
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Zurücksetzen erfolgreich"})
return navigateTo("/login")
} catch (err: any) {
toast.add({title:"Problem beim zurücksetzen",color:"rose"})
}
}
</script>
<template>
<UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAuthForm
title="Passwort zurücksetzen"
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
align="bottom"
:fields="[{
name: 'email',
type: 'text',
label: 'Email',
placeholder: 'Deine E-Mail Adresse'
}]"
:loading="false"
@submit="doReset"
:submit-button="{label: 'Zurücksetzen'}"
divider="oder"
>
</UAuthForm>
</UCard>
</template>

View File

@@ -3,12 +3,12 @@ import {setPageLayout} from "#app";
import {useCapacitor} from "~/composables/useCapacitor.js"; import {useCapacitor} from "~/composables/useCapacitor.js";
definePageMeta({ definePageMeta({
middleware: "auth",
layout: "default", layout: "default",
}) })
const route = useRoute() const route = useRoute()
const dataStore = useDataStore() const dataStore = useDataStore()
const supabase = useSupabaseClient() const api = useNuxtApp().$api
const type = route.params.type const type = route.params.type
const platform = await useCapacitor().getIsPhone() ? "mobile" : "default" const platform = await useCapacitor().getIsPhone() ? "mobile" : "default"
@@ -32,12 +32,14 @@ const setupPage = async () => {
if(mode.value === "show") { if(mode.value === "show") {
//Load Data for Show //Load Data for Show
item.value = await useSupabaseSelectSingle(type, route.params.id, dataType.supabaseSelectWithInformation || "*") //item.value = await useSupabaseSelectSingle(type, route.params.id, dataType.supabaseSelectWithInformation || "*")
item.value = await useEntities(type).selectSingle(route.params.id,"*",true)
} else if(mode.value === "edit") { } else if(mode.value === "edit") {
//Load Data for Edit //Load Data for Edit
const data = JSON.stringify((await supabase.from(type).select().eq("id", route.params.id).single()).data) //const data = JSON.stringify((await supabase.from(type).select().eq("id", route.params.id).single()).data)
//await useSupabaseSelectSingle(type, route.params.id) //await useSupabaseSelectSingle(type, route.params.id)
item.value = data item.value = JSON.stringify(await useEntities(type).selectSingle(route.params.id))
//item.value = data
} else if(mode.value === "create") { } else if(mode.value === "create") {
//Load Data for Create //Load Data for Create
@@ -46,7 +48,7 @@ const setupPage = async () => {
console.log(item.value) console.log(item.value)
} else if(mode.value === "list") { } else if(mode.value === "list") {
//Load Data for List //Load Data for List
items.value = await useSupabaseSelect(type, dataType.supabaseSelectWithInformation || "*", dataType.supabaseSortColumn,dataType.supabaseSortAscending || false, true) items.value = await useEntities(type).select()
} }
loaded.value = true loaded.value = true

24
plugins/api.ts Normal file
View File

@@ -0,0 +1,24 @@
export default defineNuxtPlugin(() => {
const api = $fetch.create({
baseURL: "http://localhost:3100",
credentials: "include",
onRequest({ options }) {
// Token aus Cookie holen
let token = useCookie("token").value
// Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden
if (options.context && (options.context as any).jwt) {
token = (options.context as any).jwt
}
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
}
}
}
})
return { provide: { api } }
})

View File

@@ -0,0 +1,4 @@
export default defineNuxtPlugin(async () => {
const auth = useAuthStore()
await auth.init()
})

84
stores/auth.ts Normal file
View File

@@ -0,0 +1,84 @@
import { defineStore } from "pinia"
export const useAuthStore = defineStore("auth", {
state: () => ({
user: null as null | { user_id: string; email: string; tenant_id?: string; role?: string },
profile: null as null | any,
tenants: [] as { tenant_id: string; role: string; tenants: { id: string; name: string } }[],
permissions: [] as string[],
activeTenant: null as any,
loading: true as boolean,
}),
actions: {
async init(token) {
await this.fetchMe(token)
},
async login(email: string, password: string) {
const { token } = await useNuxtApp().$api("/auth/login", {
method: "POST",
body: { email, password }
})
useCookie("token").value = token // persistieren
await this.fetchMe(token)
},
async logout() {
try {
await useNuxtApp().$api("/auth/logout", { method: "POST" })
} catch (e) {
console.error("Logout fehlgeschlagen:", e)
}
this.user = null
this.permissions = []
this.profile = null
this.activeTenant = null
this.tenants = []
useCookie("token").value = null
navigateTo("/login")
},
async fetchMe(jwt= null) {
try {
const me = await useNuxtApp().$api("/api/me", {
headers: { Authorization: `Bearer ${jwt}`,
context: {
jwt
}}
})
console.log(me)
this.user = me.user
this.permissions = me.permissions
this.tenants = me.tenants
this.profile = me.profile
if(me.activeTenant) {
this.activeTenant = me.activeTenant
this.loading = false
} else {
}
} catch (err: any) {
if (err?.response?.status === 401) this.logout()
}
},
async switchTenant(tenant_id: string) {
this.loading = true
const { token } = await useNuxtApp().$api("/api/tenant/switch", {
method: "POST",
body: { tenant_id }
})
useCookie("token").value = token
await this.init(token)
},
hasPermission(key: string) {
return this.permissions.includes(key)
}
}
})

File diff suppressed because it is too large Load Diff