New Backend changes
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;">
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
27
components/TenantDropdown.vue
Normal file
27
components/TenantDropdown.vue
Normal 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>
|
||||||
@@ -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
122
composables/useEntities.ts
Normal 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
9
composables/usePermission.ts
Normal file
9
composables/usePermission.ts
Normal 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
28
composables/useUsers.ts
Normal 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
17
middleware/auth.global.ts
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
136
pages/login.vue
136
pages/login.vue
@@ -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
62
pages/password-change.vue
Normal 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
55
pages/password-reset.vue
Normal 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>
|
||||||
@@ -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
24
plugins/api.ts
Normal 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 } }
|
||||||
|
})
|
||||||
4
plugins/auth-init.client.ts
Normal file
4
plugins/auth-init.client.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
await auth.init()
|
||||||
|
})
|
||||||
84
stores/auth.ts
Normal file
84
stores/auth.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
1054
stores/data.js
1054
stores/data.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user