Compare commits

...

2 Commits

Author SHA1 Message Date
b4ec792cc0 Diasbled Label Test Card
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Successful in 1m7s
2026-01-15 19:09:26 +01:00
9b3f48defe Added Calculator 2026-01-15 19:08:26 +01:00
5 changed files with 459 additions and 288 deletions

View File

@@ -0,0 +1,235 @@
<template>
<div
ref="el"
:style="style"
class="fixed z-[999] w-72 bg-white dark:bg-gray-900 shadow-2xl rounded-xl border border-gray-200 dark:border-gray-800 p-4 select-none touch-none"
>
<div class="flex items-center justify-between mb-4 cursor-move border-b pb-2 dark:border-gray-800">
<div class="flex items-center gap-2 text-gray-500">
<UIcon name="i-heroicons-calculator" />
<span class="text-xs font-bold uppercase tracking-wider">Kalkulator</span>
</div>
<div class="flex items-center gap-1">
<UTooltip text="Verlauf">
<UButton
color="gray"
variant="ghost"
:icon="showHistory ? 'i-heroicons-clock-solid' : 'i-heroicons-clock'"
size="xs"
@click="showHistory = !showHistory"
/>
</UTooltip>
<UTooltip text="Schließen (Esc)">
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark"
size="xs"
@click="store.isOpen = false"
/>
</UTooltip>
</div>
</div>
<div v-if="!showHistory">
<div
class="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg mb-4 text-right border border-gray-200 dark:border-gray-700 cursor-pointer group relative"
@click="copyDisplay"
>
<div class="text-[10px] text-gray-500 h-4 font-mono uppercase tracking-tighter">
Speicher: {{ Number(store.memory).toFixed(2).replace('.', ',') }}
</div>
<div class="text-2xl font-mono truncate tracking-tighter">{{ store.display }}</div>
<div class="absolute inset-0 flex items-center justify-center bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg">
<span class="text-[10px] font-bold text-primary-600 uppercase">
{{ copied ? 'Kopiert!' : 'Klicken zum Kopieren' }}
</span>
</div>
</div>
<div class="grid grid-cols-4 gap-2">
<UTooltip text="Brutto (+19%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(19)">+19%</UButton></UTooltip>
<UTooltip text="Brutto (+7%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(7)">+7%</UButton></UTooltip>
<UTooltip text="Netto (-19%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(19)">-19%</UButton></UTooltip>
<UTooltip text="Netto (-7%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(7)">-7%</UButton></UTooltip>
<UTooltip text="Löschen"><UButton color="gray" variant="ghost" block @click="clear">C</UButton></UTooltip>
<UTooltip text="Speicher +"><UButton color="gray" variant="ghost" block @click="addToSum">M+</UButton></UTooltip>
<UTooltip text="Speicher Reset"><UButton color="gray" variant="ghost" block @click="store.memory = 0">MC</UButton></UTooltip>
<UButton color="primary" variant="soft" @click="setOperator('/')">/</UButton>
<UButton v-for="n in [7, 8, 9]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
<UButton color="primary" variant="soft" @click="setOperator('*')">×</UButton>
<UButton v-for="n in [4, 5, 6]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
<UButton color="primary" variant="soft" @click="setOperator('-')">-</UButton>
<UButton v-for="n in [1, 2, 3]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
<UButton color="primary" variant="soft" @click="setOperator('+')">+</UButton>
<UButton color="white" class="col-span-2" @click="appendNumber(0)">0</UButton>
<UButton color="white" @click="addComma">,</UButton>
<UButton color="primary" block @click="calculate">=</UButton>
</div>
</div>
<div v-else class="h-[270px] flex flex-col animate-in fade-in duration-200">
<div class="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
<div v-if="store.history.length === 0" class="text-center text-gray-400 text-xs mt-10 italic">
Keine Berechnungen im Verlauf
</div>
<div
v-for="(item, i) in store.history" :key="i"
class="p-2 bg-gray-50 dark:bg-gray-800 rounded text-right border-l-2 border-primary-500 cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
@click="useHistoryItem(item.result)"
>
<div class="text-[10px] text-gray-400">{{ item.expression }} =</div>
<div class="text-sm font-bold">{{ item.result }}</div>
</div>
</div>
<UButton
color="gray"
variant="ghost"
size="xs"
block
class="mt-2"
icon="i-heroicons-trash"
@click="store.history = []"
>
Verlauf leeren
</UButton>
</div>
</div>
</template>
<script setup>
import { useDraggable, useClipboard } from '@vueuse/core'
import { useCalculatorStore } from '~/stores/calculator'
const store = useCalculatorStore()
const { copy, copied } = useClipboard()
const el = ref(null)
const { style } = useDraggable(el, {
initialValue: { x: window.innerWidth - 350, y: 150 },
})
const shouldResetDisplay = ref(false)
const showHistory = ref(false)
const previousValue = ref(null)
const lastOperator = ref(null)
// --- Logik ---
const appendNumber = (num) => {
if (store.display === '0' || shouldResetDisplay.value) {
store.display = String(num)
shouldResetDisplay.value = false
} else {
store.display += String(num)
}
}
const addComma = () => {
if (!store.display.includes(',')) {
store.display += ','
}
}
const setOperator = (op) => {
previousValue.value = parseFloat(store.display.replace(',', '.'))
lastOperator.value = op
shouldResetDisplay.value = true
}
const calculate = () => {
if (lastOperator.value === null) return
const currentVal = parseFloat(store.display.replace(',', '.'))
const prevVal = previousValue.value
let result = 0
switch (lastOperator.value) {
case '+': result = prevVal + currentVal; break
case '-': result = prevVal - currentVal; break
case '*': result = prevVal * currentVal; break
case '/': result = currentVal !== 0 ? prevVal / currentVal : 0; break
}
const expression = `${prevVal} ${lastOperator.value} ${currentVal}`
const resultString = String(Number(result.toFixed(4))).replace('.', ',')
store.addHistory(expression, resultString)
store.display = resultString
lastOperator.value = null
shouldResetDisplay.value = true
}
const clear = () => {
store.display = '0'
previousValue.value = null
lastOperator.value = null
}
const applyTax = (percent) => {
const current = parseFloat(store.display.replace(',', '.'))
store.display = (current * (1 + percent / 100)).toFixed(2).replace('.', ',')
}
const removeTax = (percent) => {
const current = parseFloat(store.display.replace(',', '.'))
store.display = (current / (1 + percent / 100)).toFixed(2).replace('.', ',')
}
const addToSum = () => {
store.memory += parseFloat(store.display.replace(',', '.'))
shouldResetDisplay.value = true
}
const copyDisplay = () => {
copy(store.display)
}
const useHistoryItem = (val) => {
store.display = val
showHistory.value = false
}
// --- Shortcuts ---
defineShortcuts({
'0': () => appendNumber(0),
'1': () => appendNumber(1),
'2': () => appendNumber(2),
'3': () => appendNumber(3),
'4': () => appendNumber(4),
'5': () => appendNumber(5),
'6': () => appendNumber(6),
'7': () => appendNumber(7),
'8': () => appendNumber(8),
'9': () => appendNumber(9),
'comma': addComma,
'plus': () => setOperator('+'),
'minus': () => setOperator('-'),
'enter': { usingInput: true, handler: calculate },
'backspace': () => {
store.display = store.display.length > 1 ? store.display.slice(0, -1) : '0'
},
// Escape schließt nun das Fenster via Store
'escape': {
usingInput: true,
whenever: [computed(() => store.isOpen)],
handler: () => { store.isOpen = false }
}
})
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
@apply bg-transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-gray-200 dark:bg-gray-700 rounded-full;
}
</style>

View File

@@ -1,42 +1,39 @@
<script setup> <script setup>
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const { has } = usePermission()
const {has} = usePermission() // Lokaler State für den Taschenrechner
const showCalculator = ref(false)
const links = computed(() => { const links = computed(() => {
return [ return [
...(auth.profile?.pinned_on_navigation || []).map(pin => { ...(auth.profile?.pinned_on_navigation || []).map(pin => {
if(pin.type === "external") { if (pin.type === "external") {
return { return {
label: pin.label, label: pin.label,
to: pin.link, to: pin.link,
icon: pin.icon, icon: pin.icon,
target: "_blank", target: "_blank",
pinned: true pinned: true
}
}else if(pin.type === "standardEntity") {
return {
label: pin.label,
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon,
pinned: true
}
} }
}), } else if (pin.type === "standardEntity") {
return {
label: pin.label,
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon,
pinned: true
}
}
}),
... false ? [{
label: "Support Tickets",
to: "/support",
icon: "i-heroicons-rectangle-stack",
}] : [],
{ {
id: 'dashboard', id: 'dashboard',
label: "Dashboard", label: "Dashboard",
to: "/", to: "/",
icon: "i-heroicons-home" icon: "i-heroicons-home"
}, { },
{
id: 'historyitems', id: 'historyitems',
label: "Logbuch", label: "Logbuch",
to: "/historyitems", to: "/historyitems",
@@ -48,31 +45,11 @@ const links = computed(() => {
icon: "i-heroicons-rectangle-stack", icon: "i-heroicons-rectangle-stack",
defaultOpen: false, defaultOpen: false,
children: [ children: [
... has("tasks") ? [{ ...has("tasks") ? [{
label: "Aufgaben", label: "Aufgaben",
to: "/standardEntity/tasks", to: "/standardEntity/tasks",
icon: "i-heroicons-rectangle-stack" icon: "i-heroicons-rectangle-stack"
}] : [], }] : [],
/*... true ? [{
label: "Plantafel",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
}] : [],
... true ? [{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
}] : [],
... true ? [{
label: "Termine",
to: "/standardEntity/events",
icon: "i-heroicons-calendar-days"
}] : [],*/
/*{
label: "Dateien",
to: "/files",
icon: "i-heroicons-document"
},*/
] ]
}, },
{ {
@@ -84,12 +61,12 @@ const links = computed(() => {
label: "Dateien", label: "Dateien",
to: "/files", to: "/files",
icon: "i-heroicons-document" icon: "i-heroicons-document"
},{ }, {
label: "Anschreiben", label: "Anschreiben",
to: "/createdletters", to: "/createdletters",
icon: "i-heroicons-document", icon: "i-heroicons-document",
disabled: true disabled: true
},{ }, {
label: "Boxen", label: "Boxen",
to: "/standardEntity/documentboxes", to: "/standardEntity/documentboxes",
icon: "i-heroicons-archive-box", icon: "i-heroicons-archive-box",
@@ -113,62 +90,44 @@ const links = computed(() => {
to: "/email/new", to: "/email/new",
icon: "i-heroicons-envelope", icon: "i-heroicons-envelope",
disabled: true disabled: true
}/*, { }
label: "Logbücher",
to: "/communication/historyItems",
icon: "i-heroicons-book-open"
}, {
label: "Chats",
to: "/chats",
icon: "i-heroicons-chat-bubble-left"
}*/
] ]
}, },
... (has("customers") || has("vendors") || has("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: [
... has("customers") ? [{ ...has("customers") ? [{
label: "Kunden", label: "Kunden",
to: "/standardEntity/customers", to: "/standardEntity/customers",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}] : [], }] : [],
... has("vendors") ? [{ ...has("vendors") ? [{
label: "Lieferanten", label: "Lieferanten",
to: "/standardEntity/vendors", to: "/standardEntity/vendors",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... has("contacts") ? [{ ...has("contacts") ? [{
label: "Ansprechpartner", label: "Ansprechpartner",
to: "/standardEntity/contacts", to: "/standardEntity/contacts",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}] : [], }] : [],
] ]
},] : [], }] : [],
{ {
label: "Mitarbeiter", label: "Mitarbeiter",
defaultOpen:false, defaultOpen: false,
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... true ? [{ ...true ? [{
label: "Zeiten", label: "Zeiten",
to: "/staff/time", to: "/staff/time",
icon: "i-heroicons-clock", icon: "i-heroicons-clock",
}] : [], }] : [],
/*... has("absencerequests") ? [{
label: "Abwesenheiten",
to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text"
}] : [],*/
/*{
label: "Fahrten",
to: "/trackingTrips",
icon: "i-heroicons-map"
},*/
] ]
}, },
... [{ ...[{
label: "Buchhaltung", label: "Buchhaltung",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-chart-bar-square", icon: "i-heroicons-chart-bar-square",
@@ -177,23 +136,23 @@ const links = computed(() => {
label: "Ausgangsbelege", label: "Ausgangsbelege",
to: "/createDocument", to: "/createDocument",
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
},{ }, {
label: "Serienvorlagen", label: "Serienvorlagen",
to: "/createDocument/serialInvoice", to: "/createDocument/serialInvoice",
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
},{ }, {
label: "Eingangsbelege", label: "Eingangsbelege",
to: "/incomingInvoices", to: "/incomingInvoices",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
},{ }, {
label: "Kostenstellen", label: "Kostenstellen",
to: "/standardEntity/costcentres", to: "/standardEntity/costcentres",
icon: "i-heroicons-document-currency-euro" icon: "i-heroicons-document-currency-euro"
},{ }, {
label: "Buchungskonten", label: "Buchungskonten",
to: "/accounts", to: "/accounts",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
},{ }, {
label: "zusätzliche Buchungskonten", label: "zusätzliche Buchungskonten",
to: "/standardEntity/ownaccounts", to: "/standardEntity/ownaccounts",
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
@@ -205,48 +164,39 @@ const links = computed(() => {
}, },
] ]
}], }],
... has("inventory") ? [{ ...has("inventory") ? [{
label: "Lager", label: "Lager",
icon: "i-heroicons-puzzle-piece", icon: "i-heroicons-puzzle-piece",
defaultOpen: false, defaultOpen: false,
children: [ children: [
/*{ ...has("spaces") ? [{
label: "Vorgänge",
to: "/inventory",
icon: "i-heroicons-square-3-stack-3d"
},{
label: "Bestände",
to: "/inventory/stocks",
icon: "i-heroicons-square-3-stack-3d"
},*/
... 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"
}] : [], }] : [],
] ]
},] : [], }] : [],
{ {
label: "Stammdaten", label: "Stammdaten",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-clipboard-document", icon: "i-heroicons-clipboard-document",
children: [ children: [
... has("products") ? [{ ...has("products") ? [{
label: "Artikel", label: "Artikel",
to: "/standardEntity/products", to: "/standardEntity/products",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... has("productcategories") ? [{ ...has("productcategories") ? [{
label: "Artikelkategorien", label: "Artikelkategorien",
to: "/standardEntity/productcategories", to: "/standardEntity/productcategories",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... has("services") ? [{ ...has("services") ? [{
label: "Leistungen", label: "Leistungen",
to: "/standardEntity/services", to: "/standardEntity/services",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
}] : [], }] : [],
... has("servicecategories") ? [{ ...has("servicecategories") ? [{
label: "Leistungskategorien", label: "Leistungskategorien",
to: "/standardEntity/servicecategories", to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
@@ -261,17 +211,17 @@ const links = computed(() => {
to: "/standardEntity/hourrates", to: "/standardEntity/hourrates",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}, },
... has("vehicles") ? [{ ...has("vehicles") ? [{
label: "Fahrzeuge", label: "Fahrzeuge",
to: "/standardEntity/vehicles", to: "/standardEntity/vehicles",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... has("inventoryitems") ? [{ ...has("inventoryitems") ? [{
label: "Inventar", label: "Inventar",
to: "/standardEntity/inventoryitems", to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... has("inventoryitems") ? [{ ...has("inventoryitems") ? [{
label: "Inventargruppen", label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups", to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
@@ -279,26 +229,21 @@ const links = computed(() => {
] ]
}, },
... has("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"
},] : [], }] : [],
... has("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"
}] : [], }] : [],
... has("plants") ? [{ ...has("plants") ? [{
label: "Objekte", label: "Objekte",
to: "/standardEntity/plants", to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
},] : [], }] : [],
/*... has("checks") ? [{
label: "Überprüfungen",
to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass"
},] : [],*/
{ {
label: "Einstellungen", label: "Einstellungen",
defaultOpen: false, defaultOpen: false,
@@ -308,67 +253,57 @@ const links = computed(() => {
label: "Nummernkreise", label: "Nummernkreise",
to: "/settings/numberRanges", to: "/settings/numberRanges",
icon: "i-heroicons-clipboard-document-list", icon: "i-heroicons-clipboard-document-list",
},/*{ }, {
label: "Rollen",
to: "/roles",
icon: "i-heroicons-key"
},*/{
label: "E-Mail Konten", label: "E-Mail Konten",
to: "/settings/emailaccounts", to: "/settings/emailaccounts",
icon: "i-heroicons-envelope", icon: "i-heroicons-envelope",
},{ }, {
label: "Bankkonten", label: "Bankkonten",
to: "/settings/banking", to: "/settings/banking",
icon: "i-heroicons-currency-euro", icon: "i-heroicons-currency-euro",
},{ }, {
label: "Textvorlagen", label: "Textvorlagen",
to: "/settings/texttemplates", to: "/settings/texttemplates",
icon: "i-heroicons-clipboard-document-list", icon: "i-heroicons-clipboard-document-list",
},/*{ }, {
label: "Eigene Felder",
to: "/settings/ownfields",
icon: "i-heroicons-clipboard-document-list"
},*/{
label: "Firmeneinstellungen", label: "Firmeneinstellungen",
to: "/settings/tenant", to: "/settings/tenant",
icon: "i-heroicons-building-office", icon: "i-heroicons-building-office",
},{ }, {
label: "Projekttypen", label: "Projekttypen",
to: "/projecttypes", to: "/projecttypes",
icon: "i-heroicons-clipboard-document-list", icon: "i-heroicons-clipboard-document-list",
},{ }, {
label: "Export", label: "Export",
to: "/export", to: "/export",
icon: "i-heroicons-clipboard-document-list" icon: "i-heroicons-clipboard-document-list"
} }
] ]
} },
] ]
}) })
// nur Items mit Children → für Accordion
const accordionItems = computed(() => const accordionItems = computed(() =>
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0) links.value.filter(item => Array.isArray(item.children) && item.children.length > 0)
) )
// nur Items ohne Children → als Buttons
const buttonItems = computed(() => const buttonItems = computed(() =>
links.value.filter(item => !item.children || item.children.length === 0) links.value.filter(item => !item.children || item.children.length === 0)
) )
</script> </script>
<template> <template>
<!-- Standalone Buttons -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<UButton <UButton
v-for="item in buttonItems" v-for="item in buttonItems"
:key="item.label" :key="item.label"
:variant="item.pinned ? 'ghost' : 'ghost'" variant="ghost"
:color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')" :color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
:icon="item.pinned ? 'i-heroicons-star' : item.icon" :icon="item.pinned ? 'i-heroicons-star' : item.icon"
class="w-full" class="w-full"
:to="item.to" :to="item.to"
:target="item.target" :target="item.target"
@click="item.click ? item.click() : null"
> >
<UIcon <UIcon
v-if="item.pinned" v-if="item.pinned"
@@ -378,8 +313,9 @@ const buttonItems = computed(() =>
{{ item.label }} {{ item.label }}
</UButton> </UButton>
</div> </div>
<UDivider/>
<!-- Accordion für die Items mit Children --> <UDivider class="my-2"/>
<UAccordion <UAccordion
:items="accordionItems" :items="accordionItems"
:multiple="false" :multiple="false"
@@ -387,7 +323,7 @@ const buttonItems = computed(() =>
> >
<template #default="{ item, open }"> <template #default="{ item, open }">
<UButton <UButton
:variant="'ghost'" variant="ghost"
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'" :color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'"
:icon="item.icon" :icon="item.icon"
class="w-full" class="w-full"
@@ -415,56 +351,13 @@ const buttonItems = computed(() =>
:to="child.to" :to="child.to"
:target="child.target" :target="child.target"
:disabled="child.disabled" :disabled="child.disabled"
@click="child.click ? child.click() : null"
> >
{{ child.label }} {{ child.label }}
</UButton> </UButton>
</div> </div>
</template> </template>
</UAccordion> </UAccordion>
<!-- <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.pinned ? 'i-heroicons-star' : item.icon"
class="w-full"
:to="item.to"
:target="item.target"
>
<UIcon
v-if="item.pinned"
:name="item.icon" class="w-5 h-5 me-2" />
{{ item.label }}
<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" v-if="item.children?.length > 0">
<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>-->
<Calculator v-if="showCalculator" v-model="showCalculator"/>
</template> </template>

View File

@@ -1,21 +1,20 @@
<script setup> <script setup>
import MainNav from "~/components/MainNav.vue"; import MainNav from "~/components/MainNav.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import GlobalMessages from "~/components/GlobalMessages.vue"; import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue"; import TenantDropdown from "~/components/TenantDropdown.vue";
import LabelPrinterButton from "~/components/LabelPrinterButton.vue"; import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
import {useCalculatorStore} from '~/stores/calculator'
const dataStore = useDataStore() const dataStore = useDataStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const { isHelpSlideoverOpen } = useDashboard() const {isHelpSlideoverOpen} = useDashboard()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const labelPrinter = useLabelPrinterStore() const labelPrinter = useLabelPrinterStore()
const calculatorStore = useCalculatorStore()
const month = dayjs().format("MM") const month = dayjs().format("MM")
@@ -24,91 +23,108 @@ const actions = [
id: 'new-customer', id: 'new-customer',
label: 'Kunde hinzufügen', label: 'Kunde hinzufügen',
icon: 'i-heroicons-user-group', icon: 'i-heroicons-user-group',
to: "/customers/create" , to: "/customers/create",
}, },
{ {
id: 'new-vendor', id: 'new-vendor',
label: 'Lieferant hinzufügen', label: 'Lieferant hinzufügen',
icon: 'i-heroicons-truck', icon: 'i-heroicons-truck',
to: "/vendors/create" , to: "/vendors/create",
}, },
{ {
id: 'new-contact', id: 'new-contact',
label: 'Ansprechpartner hinzufügen', label: 'Ansprechpartner hinzufügen',
icon: 'i-heroicons-user-group', icon: 'i-heroicons-user-group',
to: "/contacts/create" , to: "/contacts/create",
}, },
{ {
id: 'new-task', id: 'new-task',
label: 'Aufgabe hinzufügen', label: 'Aufgabe hinzufügen',
icon: 'i-heroicons-rectangle-stack', icon: 'i-heroicons-rectangle-stack',
to: "/tasks/create" , to: "/tasks/create",
}, },
{ {
id: 'new-plant', id: 'new-plant',
label: 'Objekt hinzufügen', label: 'Objekt hinzufügen',
icon: 'i-heroicons-clipboard-document', icon: 'i-heroicons-clipboard-document',
to: "/plants/create" , to: "/plants/create",
}, },
{ {
id: 'new-product', id: 'new-product',
label: 'Artikel hinzufügen', label: 'Artikel hinzufügen',
icon: 'i-heroicons-puzzle-piece', icon: 'i-heroicons-puzzle-piece',
to: "/products/create" , to: "/products/create",
}, },
{ {
id: 'new-project', id: 'new-project',
label: 'Projekt hinzufügen', label: 'Projekt hinzufügen',
icon: 'i-heroicons-clipboard-document-check', icon: 'i-heroicons-clipboard-document-check',
to: "/projects/create" , to: "/projects/create",
} }
] ]
const groups = computed(() => [ const groups = computed(() => [
{ {
key: 'actions', key: 'actions',
commands: actions commands: actions
},{ }, {
key: "customers", key: "customers",
label: "Kunden", label: "Kunden",
commands: dataStore.customers.map(item => { return {id: item.id, label: item.name, to: `/customers/show/${item.id}`}}) commands: dataStore.customers.map(item => {
},{ return {id: item.id, label: item.name, to: `/customers/show/${item.id}`}
key: "vendors", })
label: "Lieferanten", }, {
commands: dataStore.vendors.map(item => { return {id: item.id, label: item.name, to: `/vendors/show/${item.id}`}}) key: "vendors",
},{ label: "Lieferanten",
key: "contacts", commands: dataStore.vendors.map(item => {
label: "Ansprechpartner", return {id: item.id, label: item.name, to: `/vendors/show/${item.id}`}
commands: dataStore.contacts.map(item => { return {id: item.id, label: item.fullName, to: `/contacts/show/${item.id}`}}) })
},{ }, {
key: "products", key: "contacts",
label: "Artikel", label: "Ansprechpartner",
commands: dataStore.products.map(item => { return {id: item.id, label: item.name, to: `/products/show/${item.id}`}}) commands: dataStore.contacts.map(item => {
},{ return {id: item.id, label: item.fullName, to: `/contacts/show/${item.id}`}
key: "tasks", })
label: "Aufgaben", }, {
commands: dataStore.tasks.map(item => { return {id: item.id, label: item.name, to: `/tasks/show/${item.id}`}}) key: "products",
},{ label: "Artikel",
key: "plants", commands: dataStore.products.map(item => {
label: "Objekte", return {id: item.id, label: item.name, to: `/products/show/${item.id}`}
commands: dataStore.plants.map(item => { return {id: item.id, label: item.name, to: `/plants/show/${item.id}`}}) })
},{ }, {
key: "projects", key: "tasks",
label: "Projekte", label: "Aufgaben",
commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}}) commands: dataStore.tasks.map(item => {
} return {id: item.id, label: item.name, to: `/tasks/show/${item.id}`}
].filter(Boolean)) })
const footerLinks = [ }, {
/*{ key: "plants",
label: 'Invite people', label: "Objekte",
icon: 'i-heroicons-plus', commands: dataStore.plants.map(item => {
to: '/settings/members' return {id: item.id, label: item.name, to: `/plants/show/${item.id}`}
}, */{ })
label: 'Hilfe & Info', }, {
icon: 'i-heroicons-question-mark-circle', key: "projects",
click: () => isHelpSlideoverOpen.value = true label: "Projekte",
}] commands: dataStore.projects.map(item => {
return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}
})
}
].filter(Boolean))
// --- Footer Links nutzen jetzt den zentralen Calculator Store ---
const footerLinks = computed(() => [
{
label: 'Taschenrechner',
icon: 'i-heroicons-calculator',
click: () => calculatorStore.toggle()
},
{
label: 'Hilfe & Info',
icon: 'i-heroicons-question-mark-circle',
click: () => isHelpSlideoverOpen.value = true
}
])
</script> </script>
@@ -130,24 +146,24 @@ const footerLinks = [
v-else v-else
/> />
<div class="flex justify-center mb-6"> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" /> <UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500"/>
</div> </div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten Wartungsarbeiten
</h1> </h1>
<p class="text-gray-600 dark:text-gray-300 mb-8"> <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. Dieser FEDEO Mandant wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut oder verwende einen
anderen Mandanten.
</p> </p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants"> <div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}} {{ tenant.name }}
<UButton <UButton
:disabled="tenant.locked" :disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)" @click="auth.switchTenant(tenant.id)"
>Wählen</UButton> >Wählen
</UButton>
</div> </div>
</UCard> </UCard>
</UContainer> </UContainer>
</div> </div>
@@ -167,7 +183,7 @@ const footerLinks = [
v-else v-else
/> />
<div class="flex justify-center mb-6"> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" /> <UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500"/>
</div> </div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
@@ -176,8 +192,6 @@ const footerLinks = [
<p class="text-gray-600 dark:text-gray-300 mb-8"> <p class="text-gray-600 dark:text-gray-300 mb-8">
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut. FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
</p> </p>
</UCard> </UCard>
</UContainer> </UContainer>
</div> </div>
@@ -197,32 +211,33 @@ const footerLinks = [
v-else v-else
/> />
<div class="flex justify-center mb-6"> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600" /> <UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600"/>
</div> </div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Kein Aktives Abonnement für diesen Mandant. Kein Aktives Abonnement für diesen Mandant.
</h1> </h1>
<p class="text-gray-600 dark:text-gray-300 mb-8"> <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. Bitte wenden Sie sich an den FEDEO Support um ein Abonnement zu erhalten oder verwenden Sie einen anderen
Mandanten.
</p> </p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants"> <div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}} {{ tenant.name }}
<UButton <UButton
:disabled="tenant.locked" :disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)" @click="auth.switchTenant(tenant.id)"
>Wählen</UButton> >Wählen
</UButton>
</div> </div>
</UCard> </UCard>
</UContainer> </UContainer>
</div> </div>
<UDashboardLayout class="safearea" v-else > <UDashboardLayout class="safearea" v-else>
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <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' }"> <UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;"
:class="['!border-transparent']" :ui="{ left: 'flex-1' }">
<template #left> <template #left>
<TenantDropdown class="w-full" /> <TenantDropdown class="w-full"/>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
@@ -230,24 +245,17 @@ const footerLinks = [
<MainNav/> <MainNav/>
<div class="flex-1" /> <div class="flex-1"/>
<template #footer> <template #footer>
<div class="flex flex-col gap-3 w-full"> <div class="flex flex-col gap-3 w-full">
<UColorModeButton/>
<UColorModeButton />
<LabelPrinterButton/> <LabelPrinterButton/>
<UDashboardSidebarLinks :links="footerLinks"/>
<UDivider class="sticky bottom-0"/>
<!-- Footer Links -->
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/> <UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div> </div>
</template> </template>
@@ -256,14 +264,14 @@ const footerLinks = [
<UDashboardPage> <UDashboardPage>
<UDashboardPanel grow> <UDashboardPanel grow>
<slot /> <slot/>
</UDashboardPanel> </UDashboardPanel>
</UDashboardPage> </UDashboardPage>
<HelpSlideover/> <HelpSlideover/>
<Calculator v-if="calculatorStore.isOpen"/>
</UDashboardLayout> </UDashboardLayout>
</div> </div>
@@ -278,37 +286,32 @@ const footerLinks = [
v-if="month === '12'" v-if="month === '12'"
/> />
<UColorModeImage <UColorModeImage
light="/Logo.png" light="/Logo.png"
dark="/Logo_Dark.png" dark="/Logo_Dark.png"
class="w-1/3 mx-auto my-10" class="w-1/3 mx-auto my-10"
v-else v-else
/> />
<div v-if="!auth.activeTenant" class="w-1/2 mx-auto text-center"> <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>
<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">
<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>
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UButton <UButton
variant="outline" @click="auth.switchTenant(tenant.id)"
color="rose" >Wählen
</UButton>
</div>
<UButton
variant="outline"
color="rose"
@click="auth.logout()" @click="auth.logout()"
>Abmelden</UButton> >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>
</div> </div>
</template> </template>
<style scoped>
</style>

View File

@@ -56,7 +56,7 @@
> >
<display-open-tasks/> <display-open-tasks/>
</UDashboardCard> </UDashboardCard>
<UDashboardCard <!-- <UDashboardCard
title="Label Test" title="Label Test"
> >
<UButton <UButton
@@ -70,7 +70,7 @@
> >
Label Drucken Label Drucken
</UButton> </UButton>
</UDashboardCard> </UDashboardCard>-->
</UPageGrid> </UPageGrid>
</UDashboardPanelContent> </UDashboardPanelContent>
</template> </template>

View File

@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
export const useCalculatorStore = defineStore('calculator', () => {
const tempStore = useTempStore()
// Initialisierung aus dem TempStore
const isOpen = ref(false)
const display = computed({
get: () => tempStore.settings?.calculator?.display || '0',
set: (val) => tempStore.modifySettings('calculator', { ...tempStore.settings.calculator, display: val })
})
const memory = computed({
get: () => tempStore.settings?.calculator?.memory || 0,
set: (val) => tempStore.modifySettings('calculator', { ...tempStore.settings.calculator, memory: val })
})
const history = computed({
get: () => tempStore.filters?.calculator?.history || [],
set: (val) => tempStore.modifyFilter('calculator', 'history', val)
})
function toggle() {
isOpen.value = !isOpen.value
}
function addHistory(expression: string, result: string) {
const newHistory = [{ expression, result }, ...history.value].slice(0, 10)
history.value = newHistory
}
return {
isOpen,
display,
memory,
history,
toggle,
addHistory
}
})