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 route = useRoute()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const modal = useModal()
const dataType = dataStore.dataTypes[type]
const openTab = ref(0)
const item = ref(JSON.parse(props.item))
console.log(item.value)
@@ -156,7 +154,7 @@ const loadOptions = async () => {
} else if(option.option === "units") {
loadedOptions.value[option.option] = (await supabase.from("units").select()).data
} 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){
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) {
ret = await dataStore.createNewItem(type,item.value,true)
} else {
ret = dataStore.createNewItem(type,item.value)
ret = await useEntities(type).create(item.value)//dataStore.createNewItem(type,item.value)
}
emit('returnData', ret)
@@ -222,7 +220,8 @@ const updateItem = async () => {
if(props.inModal) {
ret = await dataStore.updateItem(type,item.value, oldItem.value,true)
} 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)

View File

@@ -4,6 +4,9 @@ import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
import EntityTable from "~/components/EntityTable.vue";
import EntityListMobile from "~/components/EntityListMobile.vue";
const { has } = usePermission()
const props = defineProps({
type: {
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)
})
</script>
@@ -139,7 +125,7 @@ const filteredRows = computed(() => {
/>
<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`)"
class="ml-3"
>+ {{dataType.labelSingle}}</UButton>

View File

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

View File

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

View File

@@ -14,8 +14,9 @@ const props = defineProps({
default: false
}
})
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const auth = useAuthStore()
const toast = useToast()
const showAddHistoryItemModal = ref(false)
const colorMode = useColorMode()
@@ -28,11 +29,12 @@ const setup = async () => {
if(await useCapacitor().getIsPhone()) platform.value = "mobile"
if(props.type && props.elementId){
items.value = (await supabase.from("historyitems").select().eq(props.type,props.elementId).order("created_at",{ascending: true})).data || []
} else {
//items.value = (await supabase.from("historyitems").select().eq(props.type,props.elementId).order("created_at",{ascending: true})).data || []
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 || []
}
}*/
}
setup()
@@ -40,55 +42,24 @@ setup()
const addHistoryItemData = ref({
text: "",
config: {
type: props.type,
id: props.elementId
}
text: ""
})
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 => {
let rawUsername = i[1]
addHistoryItemData.value = {}
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">
<UAvatar
v-if="!item.createdBy"
v-if="!item.created_by"
:src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' "
/>
<UAvatar
:alt="profileStore.getProfileById(item.createdBy).fullName"
:alt="item.created_by_profile?.full_name"
v-else
/>
<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>
<span v-html="renderText(item.text)"/><br>
<span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span>

View File

@@ -1,19 +1,25 @@
<script setup>
import {useRole} from "~/composables/useRole.js";
const profileStore = useProfileStore()
const route = useRoute()
const auth = useAuthStore()
const {has} = usePermission()
const role = useRole()
const links = computed(() => {
return [
... profileStore.currentTenant === 21 ? [{
label: "Lieferschein Formular",
to: "https://loar.ma-agrarservice.de/erfassung",
icon: "i-heroicons-rectangle-stack",
target: "_blank",
}] : [],
... profileStore.currentTenant === 5 ? [{
...auth.profile.pinned_on_navigation.map(pin => {
if(pin.type === "external") {
return {
label: pin.label,
to: pin.link,
icon: pin.icon,
target: "_blank",
pinned: true
}
}
}),
... false ? [{
label: "Support Tickets",
to: "/support",
icon: "i-heroicons-rectangle-stack",
@@ -34,22 +40,22 @@ const links = computed(() => {
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: [
... role.checkRight("tasks") ? [{
... has("tasks") ? [{
label: "Aufgaben",
to: "/standardEntity/tasks",
icon: "i-heroicons-rectangle-stack"
}] : [],
... profileStore.ownTenant.features.planningBoard ? [{
... true ? [{
label: "Plantafel",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
}] : [],
... profileStore.ownTenant.features.calendar ? [{
... true ? [{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
}] : [],
... profileStore.ownTenant.features.calendar ? [{
... true ? [{
label: "Termine",
to: "/standardEntity/events",
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",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: [
... role.checkRight("customers") ? [{
... has("customers") ? [{
label: "Kunden",
to: "/standardEntity/customers",
icon: "i-heroicons-user-group"
}] : [],
... role.checkRight("vendors") ? [{
... has("vendors") ? [{
label: "Lieferanten",
to: "/standardEntity/vendors",
icon: "i-heroicons-truck"
}] : [],
... role.checkRight("contacts") ? [{
... has("contacts") ? [{
label: "Ansprechpartner",
to: "/standardEntity/contacts",
icon: "i-heroicons-user-group"
@@ -124,17 +130,17 @@ const links = computed(() => {
defaultOpen:false,
icon: "i-heroicons-user-group",
children: [
... profileStore.ownTenant.features.timeTracking ? [{
... true ? [{
label: "Projektzeiten",
to: "/times",
icon: "i-heroicons-clock"
}] : [],
... profileStore.ownTenant.features.workingTimeTracking ? [{
... true ? [{
label: "Anwesenheiten",
to: "/workingtimes",
icon: "i-heroicons-clock"
}] : [],
... role.checkRight("absencerequests") ? [{
... has("absencerequests") ? [{
label: "Abwesenheiten",
to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text"
@@ -146,7 +152,7 @@ const links = computed(() => {
},
]
},
... profileStore.ownTenant.features.accounting ? [{
... true ? [{
label: "Buchhaltung",
defaultOpen: false,
icon: "i-heroicons-chart-bar-square",
@@ -183,7 +189,7 @@ const links = computed(() => {
},
]
},] : [],
... role.checkRight("inventory") ? [{
... has("inventory") ? [{
label: "Lager",
icon: "i-heroicons-puzzle-piece",
defaultOpen: false,
@@ -197,7 +203,7 @@ const links = computed(() => {
to: "/inventory/stocks",
icon: "i-heroicons-square-3-stack-3d"
},*/
... role.checkRight("spaces") ? [{
... has("spaces") ? [{
label: "Lagerplätze",
to: "/standardEntity/spaces",
icon: "i-heroicons-square-3-stack-3d"
@@ -209,22 +215,22 @@ const links = computed(() => {
defaultOpen: false,
icon: "i-heroicons-clipboard-document",
children: [
... role.checkRight("products") ? [{
... has("products") ? [{
label: "Artikel",
to: "/standardEntity/products",
icon: "i-heroicons-puzzle-piece"
}] : [],
... role.checkRight("productcategories") ? [{
... has("productcategories") ? [{
label: "Artikelkategorien",
to: "/standardEntity/productcategories",
icon: "i-heroicons-puzzle-piece"
}] : [],
... role.checkRight("services") ? [{
... has("services") ? [{
label: "Leistungen",
to: "/standardEntity/services",
icon: "i-heroicons-wrench-screwdriver"
}] : [],
... role.checkRight("servicecategories") ? [{
... has("servicecategories") ? [{
label: "Leistungskategorien",
to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver"
@@ -239,39 +245,39 @@ const links = computed(() => {
to: "/standardEntity/hourrates",
icon: "i-heroicons-user-group"
},
... role.checkRight("vehicles") ? [{
... has("vehicles") ? [{
label: "Fahrzeuge",
to: "/standardEntity/vehicles",
icon: "i-heroicons-truck"
}] : [],
... role.checkRight("inventoryitems") ? [{
... has("inventoryitems") ? [{
label: "Inventar",
to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece"
}] : [],
... role.checkRight("inventoryitems") ? [{
... has("inventoryitems") ? [{
label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece"
}] : [],
]
},
... role.checkRight("checks") ? [{
... has("checks") ? [{
label: "Überprüfungen",
to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass"
},] : [],
... role.checkRight("projects") ? [{
... has("projects") ? [{
label: "Projekte",
to: "/standardEntity/projects",
icon: "i-heroicons-clipboard-document-check"
},] : [],
... role.checkRight("contracts") ? [{
... has("contracts") ? [{
label: "Verträge",
to: "/standardEntity/contracts",
icon: "i-heroicons-clipboard-document"
}] : [],
... role.checkRight("plants") ? [{
... has("plants") ? [{
label: "Objekte",
to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document"
@@ -331,8 +337,49 @@ const links = computed(() => {
</script>
<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"
>
<UAccordion
@@ -382,5 +429,5 @@ const links = computed(() => {
>
{{item.label}}
</UButton>
</div>
</div>-->
</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>
const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts()
@@ -7,23 +8,26 @@ const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
const auth = useAuthStore()
const items = computed(() => [
[{
slot: 'account',
label: '',
disabled: true
}], [{
}], [/*{
label: 'Mein Profil',
icon: 'i-heroicons-user',
to: `/profiles/show/${profileStore.activeProfile.id}`
},*/{
label: 'Passwort ändern',
icon: 'i-heroicons-shield-check',
to: `/password-change`
},{
label: 'Abmelden',
icon: 'i-heroicons-arrow-left-on-rectangle',
click: async () => {
await supabase.auth.signOut()
//await dataStore.clearStore()
await router.push('/login')
await auth.logout()
}
}]
@@ -33,10 +37,10 @@ const items = computed(() => [
<template>
<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 }">
<UButton color="gray" variant="ghost" class="w-full" :label="profileStore.activeProfile.fullName" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<template #leading>
<UAvatar :alt="profileStore.activeProfile ? profileStore.activeProfile.fullName : ''" size="xs" />
</template>
<UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<!-- <template #leading>
<UAvatar :alt="auth.user.email" size="xs" />
</template>-->
<template #trailing>
<UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" />
@@ -50,7 +54,7 @@ const items = computed(() => [
Angemeldet als
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{profileStore.activeProfile.email}}
{{auth.user.email}}
</p>
</div>
</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 dayjs from "dayjs";
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const colorMode = useColorMode()
const { isHelpSlideoverOpen } = useDashboard()
const supabase = useSupabaseClient()
const router = useRouter()
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")
@@ -65,8 +65,8 @@ const actions = [
]
const groups = computed(() =>
[{
const groups = computed(() => [
{
key: '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}`}})
}
].filter(Boolean))
const footerLinks = [/*{
const footerLinks = [
/*{
label: 'Invite people',
icon: 'i-heroicons-plus',
to: '/settings/members'
@@ -112,56 +113,163 @@ const footerLinks = [/*{
</script>
<template>
<UDashboardLayout class="safearea" v-if="profileStore.loaded">
<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>
<ProfileDropdown 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 v-if="!auth.loading">
<div v-if="auth.tenants.find(t => t.id === auth.activeTenant).locked === 'maintenance_tenant'">
<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>
</template>
</UDashboardSidebar>
</UDashboardPanel>
<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">
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>
<slot />
</UCard>
</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>
</UDashboardPage>
<UDashboardPage>
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<!-- ~/components/HelpSlideover.vue -->
<HelpSlideover/>
<!-- ~/components/NotificationsSlideover.vue -->
<NotificationsSlideover />
<HelpSlideover/>
<!--<NotificationsSlideover />
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>-->
</UDashboardLayout>
</div>
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>
</UDashboardLayout>
<div
v-else
class="flex flex-col"
@@ -178,12 +286,25 @@ const footerLinks = [/*{
class="w-1/3 mx-auto my-10"
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 v-else>
<UProgress animation="carousel" class="w-3/4 mx-auto mt-10" />
</div>
@@ -192,7 +313,5 @@ const footerLinks = [/*{
</template>
<style scoped>
.testclass {
color: red;
}
</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: {
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
url: "https://uwppvcxflrcsibuzsbil.supabase.co"
url: "https://uwppvcxflrcsibuzsbil.supabase.co",
redirect:false
},
vite: {

View File

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

View File

@@ -1,118 +1,32 @@
<script setup >
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "~/composables/useCapacitor.js";
<script setup lang="ts">
definePageMeta({
layout: "notLoggedIn"
})
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const router = useRouter()
const colorMode = useColorMode()
const auth = useAuthStore()
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>
<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">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAuthForm
@@ -131,29 +45,13 @@ const onSubmit = async (data) => {
placeholder: 'Dein Passwort'
}]"
:loading="false"
@submit="onSubmit"
:providers="[{label: 'MS365',icon: 'i-simple-icons-microsoft',color: 'gray',click: authenticateWithAzure}]"
@submit="doLogin"
:submit-button="{label: 'Weiter'}"
divider="oder"
>
<template #password-hint>
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
</template>
</UAuthForm>
</UCard>
</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";
definePageMeta({
middleware: "auth",
layout: "default",
})
const route = useRoute()
const dataStore = useDataStore()
const supabase = useSupabaseClient()
const api = useNuxtApp().$api
const type = route.params.type
const platform = await useCapacitor().getIsPhone() ? "mobile" : "default"
@@ -32,12 +32,14 @@ const setupPage = async () => {
if(mode.value === "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") {
//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)
item.value = data
item.value = JSON.stringify(await useEntities(type).selectSingle(route.params.id))
//item.value = data
} else if(mode.value === "create") {
//Load Data for Create
@@ -46,7 +48,7 @@ const setupPage = async () => {
console.log(item.value)
} else if(mode.value === "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

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