New Backend changes
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
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
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 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
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: {
|
||||
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
|
||||
url: "https://uwppvcxflrcsibuzsbil.supabase.co"
|
||||
url: "https://uwppvcxflrcsibuzsbil.supabase.co",
|
||||
redirect:false
|
||||
},
|
||||
|
||||
vite: {
|
||||
|
||||
@@ -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()
|
||||
|
||||
136
pages/login.vue
136
pages/login.vue
@@ -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>
|
||||
</template>
|
||||
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";
|
||||
|
||||
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
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