Added Calculator
This commit is contained in:
235
frontend/components/Calculator.vue
Normal file
235
frontend/components/Calculator.vue
Normal 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>
|
||||
@@ -1,42 +1,39 @@
|
||||
<script setup>
|
||||
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const { has } = usePermission()
|
||||
|
||||
const {has} = usePermission()
|
||||
// Lokaler State für den Taschenrechner
|
||||
const showCalculator = ref(false)
|
||||
|
||||
const links = computed(() => {
|
||||
return [
|
||||
...(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
|
||||
}
|
||||
}else if(pin.type === "standardEntity") {
|
||||
return {
|
||||
label: pin.label,
|
||||
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
|
||||
icon: pin.icon,
|
||||
pinned: true
|
||||
}
|
||||
...(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
|
||||
}
|
||||
}),
|
||||
} 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',
|
||||
label: "Dashboard",
|
||||
to: "/",
|
||||
icon: "i-heroicons-home"
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: 'historyitems',
|
||||
label: "Logbuch",
|
||||
to: "/historyitems",
|
||||
@@ -48,31 +45,11 @@ const links = computed(() => {
|
||||
icon: "i-heroicons-rectangle-stack",
|
||||
defaultOpen: false,
|
||||
children: [
|
||||
... has("tasks") ? [{
|
||||
...has("tasks") ? [{
|
||||
label: "Aufgaben",
|
||||
to: "/standardEntity/tasks",
|
||||
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",
|
||||
to: "/files",
|
||||
icon: "i-heroicons-document"
|
||||
},{
|
||||
}, {
|
||||
label: "Anschreiben",
|
||||
to: "/createdletters",
|
||||
icon: "i-heroicons-document",
|
||||
disabled: true
|
||||
},{
|
||||
}, {
|
||||
label: "Boxen",
|
||||
to: "/standardEntity/documentboxes",
|
||||
icon: "i-heroicons-archive-box",
|
||||
@@ -113,62 +90,44 @@ const links = computed(() => {
|
||||
to: "/email/new",
|
||||
icon: "i-heroicons-envelope",
|
||||
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",
|
||||
defaultOpen: false,
|
||||
icon: "i-heroicons-user-group",
|
||||
children: [
|
||||
... has("customers") ? [{
|
||||
...has("customers") ? [{
|
||||
label: "Kunden",
|
||||
to: "/standardEntity/customers",
|
||||
icon: "i-heroicons-user-group"
|
||||
}] : [],
|
||||
... has("vendors") ? [{
|
||||
...has("vendors") ? [{
|
||||
label: "Lieferanten",
|
||||
to: "/standardEntity/vendors",
|
||||
icon: "i-heroicons-truck"
|
||||
}] : [],
|
||||
... has("contacts") ? [{
|
||||
...has("contacts") ? [{
|
||||
label: "Ansprechpartner",
|
||||
to: "/standardEntity/contacts",
|
||||
icon: "i-heroicons-user-group"
|
||||
}] : [],
|
||||
]
|
||||
},] : [],
|
||||
}] : [],
|
||||
{
|
||||
label: "Mitarbeiter",
|
||||
defaultOpen:false,
|
||||
defaultOpen: false,
|
||||
icon: "i-heroicons-user-group",
|
||||
children: [
|
||||
... true ? [{
|
||||
...true ? [{
|
||||
label: "Zeiten",
|
||||
to: "/staff/time",
|
||||
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",
|
||||
defaultOpen: false,
|
||||
icon: "i-heroicons-chart-bar-square",
|
||||
@@ -177,23 +136,23 @@ const links = computed(() => {
|
||||
label: "Ausgangsbelege",
|
||||
to: "/createDocument",
|
||||
icon: "i-heroicons-document-text"
|
||||
},{
|
||||
}, {
|
||||
label: "Serienvorlagen",
|
||||
to: "/createDocument/serialInvoice",
|
||||
icon: "i-heroicons-document-text"
|
||||
},{
|
||||
}, {
|
||||
label: "Eingangsbelege",
|
||||
to: "/incomingInvoices",
|
||||
icon: "i-heroicons-document-text",
|
||||
},{
|
||||
}, {
|
||||
label: "Kostenstellen",
|
||||
to: "/standardEntity/costcentres",
|
||||
icon: "i-heroicons-document-currency-euro"
|
||||
},{
|
||||
}, {
|
||||
label: "Buchungskonten",
|
||||
to: "/accounts",
|
||||
icon: "i-heroicons-document-text",
|
||||
},{
|
||||
}, {
|
||||
label: "zusätzliche Buchungskonten",
|
||||
to: "/standardEntity/ownaccounts",
|
||||
icon: "i-heroicons-document-text"
|
||||
@@ -205,48 +164,39 @@ const links = computed(() => {
|
||||
},
|
||||
]
|
||||
}],
|
||||
... has("inventory") ? [{
|
||||
...has("inventory") ? [{
|
||||
label: "Lager",
|
||||
icon: "i-heroicons-puzzle-piece",
|
||||
defaultOpen: false,
|
||||
children: [
|
||||
/*{
|
||||
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") ? [{
|
||||
...has("spaces") ? [{
|
||||
label: "Lagerplätze",
|
||||
to: "/standardEntity/spaces",
|
||||
icon: "i-heroicons-square-3-stack-3d"
|
||||
}] : [],
|
||||
]
|
||||
},] : [],
|
||||
}] : [],
|
||||
{
|
||||
label: "Stammdaten",
|
||||
defaultOpen: false,
|
||||
icon: "i-heroicons-clipboard-document",
|
||||
children: [
|
||||
... has("products") ? [{
|
||||
...has("products") ? [{
|
||||
label: "Artikel",
|
||||
to: "/standardEntity/products",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
}] : [],
|
||||
... has("productcategories") ? [{
|
||||
...has("productcategories") ? [{
|
||||
label: "Artikelkategorien",
|
||||
to: "/standardEntity/productcategories",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
}] : [],
|
||||
... has("services") ? [{
|
||||
...has("services") ? [{
|
||||
label: "Leistungen",
|
||||
to: "/standardEntity/services",
|
||||
icon: "i-heroicons-wrench-screwdriver"
|
||||
}] : [],
|
||||
... has("servicecategories") ? [{
|
||||
...has("servicecategories") ? [{
|
||||
label: "Leistungskategorien",
|
||||
to: "/standardEntity/servicecategories",
|
||||
icon: "i-heroicons-wrench-screwdriver"
|
||||
@@ -261,17 +211,17 @@ const links = computed(() => {
|
||||
to: "/standardEntity/hourrates",
|
||||
icon: "i-heroicons-user-group"
|
||||
},
|
||||
... has("vehicles") ? [{
|
||||
...has("vehicles") ? [{
|
||||
label: "Fahrzeuge",
|
||||
to: "/standardEntity/vehicles",
|
||||
icon: "i-heroicons-truck"
|
||||
}] : [],
|
||||
... has("inventoryitems") ? [{
|
||||
...has("inventoryitems") ? [{
|
||||
label: "Inventar",
|
||||
to: "/standardEntity/inventoryitems",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
}] : [],
|
||||
... has("inventoryitems") ? [{
|
||||
...has("inventoryitems") ? [{
|
||||
label: "Inventargruppen",
|
||||
to: "/standardEntity/inventoryitemgroups",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
@@ -279,26 +229,21 @@ const links = computed(() => {
|
||||
]
|
||||
},
|
||||
|
||||
... has("projects") ? [{
|
||||
...has("projects") ? [{
|
||||
label: "Projekte",
|
||||
to: "/standardEntity/projects",
|
||||
icon: "i-heroicons-clipboard-document-check"
|
||||
},] : [],
|
||||
... has("contracts") ? [{
|
||||
}] : [],
|
||||
...has("contracts") ? [{
|
||||
label: "Verträge",
|
||||
to: "/standardEntity/contracts",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
}] : [],
|
||||
... has("plants") ? [{
|
||||
...has("plants") ? [{
|
||||
label: "Objekte",
|
||||
to: "/standardEntity/plants",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
},] : [],
|
||||
/*... has("checks") ? [{
|
||||
label: "Überprüfungen",
|
||||
to: "/standardEntity/checks",
|
||||
icon: "i-heroicons-magnifying-glass"
|
||||
},] : [],*/
|
||||
}] : [],
|
||||
{
|
||||
label: "Einstellungen",
|
||||
defaultOpen: false,
|
||||
@@ -308,67 +253,57 @@ const links = computed(() => {
|
||||
label: "Nummernkreise",
|
||||
to: "/settings/numberRanges",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},/*{
|
||||
label: "Rollen",
|
||||
to: "/roles",
|
||||
icon: "i-heroicons-key"
|
||||
},*/{
|
||||
}, {
|
||||
label: "E-Mail Konten",
|
||||
to: "/settings/emailaccounts",
|
||||
icon: "i-heroicons-envelope",
|
||||
},{
|
||||
}, {
|
||||
label: "Bankkonten",
|
||||
to: "/settings/banking",
|
||||
icon: "i-heroicons-currency-euro",
|
||||
},{
|
||||
}, {
|
||||
label: "Textvorlagen",
|
||||
to: "/settings/texttemplates",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},/*{
|
||||
label: "Eigene Felder",
|
||||
to: "/settings/ownfields",
|
||||
icon: "i-heroicons-clipboard-document-list"
|
||||
},*/{
|
||||
}, {
|
||||
label: "Firmeneinstellungen",
|
||||
to: "/settings/tenant",
|
||||
icon: "i-heroicons-building-office",
|
||||
},{
|
||||
}, {
|
||||
label: "Projekttypen",
|
||||
to: "/projecttypes",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},{
|
||||
}, {
|
||||
label: "Export",
|
||||
to: "/export",
|
||||
icon: "i-heroicons-clipboard-document-list"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// nur Items mit Children → für Accordion
|
||||
const accordionItems = computed(() =>
|
||||
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0)
|
||||
)
|
||||
|
||||
// nur Items ohne Children → als Buttons
|
||||
const buttonItems = computed(() =>
|
||||
links.value.filter(item => !item.children || item.children.length === 0)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Standalone Buttons -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<UButton
|
||||
v-for="item in buttonItems"
|
||||
:key="item.label"
|
||||
:variant="item.pinned ? 'ghost' : 'ghost'"
|
||||
variant="ghost"
|
||||
:color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
|
||||
:icon="item.pinned ? 'i-heroicons-star' : item.icon"
|
||||
class="w-full"
|
||||
:to="item.to"
|
||||
:target="item.target"
|
||||
@click="item.click ? item.click() : null"
|
||||
>
|
||||
<UIcon
|
||||
v-if="item.pinned"
|
||||
@@ -378,8 +313,9 @@ const buttonItems = computed(() =>
|
||||
{{ item.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
<UDivider/>
|
||||
<!-- Accordion für die Items mit Children -->
|
||||
|
||||
<UDivider class="my-2"/>
|
||||
|
||||
<UAccordion
|
||||
:items="accordionItems"
|
||||
:multiple="false"
|
||||
@@ -387,7 +323,7 @@ const buttonItems = computed(() =>
|
||||
>
|
||||
<template #default="{ item, open }">
|
||||
<UButton
|
||||
:variant="'ghost'"
|
||||
variant="ghost"
|
||||
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'"
|
||||
:icon="item.icon"
|
||||
class="w-full"
|
||||
@@ -415,56 +351,13 @@ const buttonItems = computed(() =>
|
||||
:to="child.to"
|
||||
:target="child.target"
|
||||
:disabled="child.disabled"
|
||||
@click="child.click ? child.click() : null"
|
||||
>
|
||||
{{ child.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup>
|
||||
|
||||
|
||||
import MainNav from "~/components/MainNav.vue";
|
||||
import dayjs from "dayjs";
|
||||
import GlobalMessages from "~/components/GlobalMessages.vue";
|
||||
import TenantDropdown from "~/components/TenantDropdown.vue";
|
||||
import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
|
||||
import {useCalculatorStore} from '~/stores/calculator'
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const colorMode = useColorMode()
|
||||
const { isHelpSlideoverOpen } = useDashboard()
|
||||
const {isHelpSlideoverOpen} = useDashboard()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const auth = useAuthStore()
|
||||
const labelPrinter = useLabelPrinterStore()
|
||||
|
||||
const calculatorStore = useCalculatorStore()
|
||||
|
||||
const month = dayjs().format("MM")
|
||||
|
||||
@@ -24,91 +23,108 @@ const actions = [
|
||||
id: 'new-customer',
|
||||
label: 'Kunde hinzufügen',
|
||||
icon: 'i-heroicons-user-group',
|
||||
to: "/customers/create" ,
|
||||
to: "/customers/create",
|
||||
},
|
||||
{
|
||||
id: 'new-vendor',
|
||||
label: 'Lieferant hinzufügen',
|
||||
icon: 'i-heroicons-truck',
|
||||
to: "/vendors/create" ,
|
||||
to: "/vendors/create",
|
||||
},
|
||||
{
|
||||
id: 'new-contact',
|
||||
label: 'Ansprechpartner hinzufügen',
|
||||
icon: 'i-heroicons-user-group',
|
||||
to: "/contacts/create" ,
|
||||
to: "/contacts/create",
|
||||
},
|
||||
{
|
||||
id: 'new-task',
|
||||
label: 'Aufgabe hinzufügen',
|
||||
icon: 'i-heroicons-rectangle-stack',
|
||||
to: "/tasks/create" ,
|
||||
to: "/tasks/create",
|
||||
},
|
||||
{
|
||||
id: 'new-plant',
|
||||
label: 'Objekt hinzufügen',
|
||||
icon: 'i-heroicons-clipboard-document',
|
||||
to: "/plants/create" ,
|
||||
to: "/plants/create",
|
||||
},
|
||||
{
|
||||
id: 'new-product',
|
||||
label: 'Artikel hinzufügen',
|
||||
icon: 'i-heroicons-puzzle-piece',
|
||||
to: "/products/create" ,
|
||||
to: "/products/create",
|
||||
},
|
||||
{
|
||||
id: 'new-project',
|
||||
label: 'Projekt hinzufügen',
|
||||
icon: 'i-heroicons-clipboard-document-check',
|
||||
to: "/projects/create" ,
|
||||
to: "/projects/create",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const groups = computed(() => [
|
||||
{
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
},{
|
||||
key: "customers",
|
||||
label: "Kunden",
|
||||
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: "contacts",
|
||||
label: "Ansprechpartner",
|
||||
commands: dataStore.contacts.map(item => { return {id: item.id, label: item.fullName, to: `/contacts/show/${item.id}`}})
|
||||
},{
|
||||
key: "products",
|
||||
label: "Artikel",
|
||||
commands: dataStore.products.map(item => { return {id: item.id, label: item.name, to: `/products/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: "plants",
|
||||
label: "Objekte",
|
||||
commands: dataStore.plants.map(item => { return {id: item.id, label: item.name, to: `/plants/show/${item.id}`}})
|
||||
},{
|
||||
key: "projects",
|
||||
label: "Projekte",
|
||||
commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}})
|
||||
}
|
||||
].filter(Boolean))
|
||||
const footerLinks = [
|
||||
/*{
|
||||
label: 'Invite people',
|
||||
icon: 'i-heroicons-plus',
|
||||
to: '/settings/members'
|
||||
}, */{
|
||||
label: 'Hilfe & Info',
|
||||
icon: 'i-heroicons-question-mark-circle',
|
||||
click: () => isHelpSlideoverOpen.value = true
|
||||
}]
|
||||
{
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
}, {
|
||||
key: "customers",
|
||||
label: "Kunden",
|
||||
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: "contacts",
|
||||
label: "Ansprechpartner",
|
||||
commands: dataStore.contacts.map(item => {
|
||||
return {id: item.id, label: item.fullName, to: `/contacts/show/${item.id}`}
|
||||
})
|
||||
}, {
|
||||
key: "products",
|
||||
label: "Artikel",
|
||||
commands: dataStore.products.map(item => {
|
||||
return {id: item.id, label: item.name, to: `/products/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: "plants",
|
||||
label: "Objekte",
|
||||
commands: dataStore.plants.map(item => {
|
||||
return {id: item.id, label: item.name, to: `/plants/show/${item.id}`}
|
||||
})
|
||||
}, {
|
||||
key: "projects",
|
||||
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>
|
||||
|
||||
@@ -130,24 +146,24 @@ const footerLinks = [
|
||||
v-else
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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.
|
||||
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}}
|
||||
{{ tenant.name }}
|
||||
<UButton
|
||||
:disabled="tenant.locked"
|
||||
@click="auth.switchTenant(tenant.id)"
|
||||
>Wählen</UButton>
|
||||
>Wählen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
|
||||
</UCard>
|
||||
</UContainer>
|
||||
</div>
|
||||
@@ -167,7 +183,7 @@ const footerLinks = [
|
||||
v-else
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
|
||||
</p>
|
||||
|
||||
|
||||
</UCard>
|
||||
</UContainer>
|
||||
</div>
|
||||
@@ -197,32 +211,33 @@ const footerLinks = [
|
||||
v-else
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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.
|
||||
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}}
|
||||
{{ tenant.name }}
|
||||
<UButton
|
||||
:disabled="tenant.locked"
|
||||
@click="auth.switchTenant(tenant.id)"
|
||||
>Wählen</UButton>
|
||||
>Wählen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
|
||||
</UCard>
|
||||
</UContainer>
|
||||
</div>
|
||||
<UDashboardLayout class="safearea" v-else >
|
||||
<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' }">
|
||||
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;"
|
||||
:class="['!border-transparent']" :ui="{ left: 'flex-1' }">
|
||||
<template #left>
|
||||
<TenantDropdown class="w-full" />
|
||||
<TenantDropdown class="w-full"/>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
@@ -230,24 +245,17 @@ const footerLinks = [
|
||||
|
||||
<MainNav/>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<div class="flex-1"/>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
|
||||
|
||||
<UColorModeButton />
|
||||
<UColorModeButton/>
|
||||
<LabelPrinterButton/>
|
||||
|
||||
<UDashboardSidebarLinks :links="footerLinks"/>
|
||||
|
||||
|
||||
<!-- Footer Links -->
|
||||
<UDashboardSidebarLinks :links="footerLinks" />
|
||||
|
||||
|
||||
|
||||
<UDivider class="sticky bottom-0" />
|
||||
<UDivider class="sticky bottom-0"/>
|
||||
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -256,14 +264,14 @@ const footerLinks = [
|
||||
|
||||
<UDashboardPage>
|
||||
<UDashboardPanel grow>
|
||||
<slot />
|
||||
<slot/>
|
||||
</UDashboardPanel>
|
||||
</UDashboardPage>
|
||||
|
||||
|
||||
|
||||
<HelpSlideover/>
|
||||
|
||||
<Calculator v-if="calculatorStore.isOpen"/>
|
||||
|
||||
</UDashboardLayout>
|
||||
</div>
|
||||
|
||||
@@ -278,37 +286,32 @@ const footerLinks = [
|
||||
v-if="month === '12'"
|
||||
/>
|
||||
<UColorModeImage
|
||||
light="/Logo.png"
|
||||
dark="/Logo_Dark.png"
|
||||
class="w-1/3 mx-auto my-10"
|
||||
v-else
|
||||
light="/Logo.png"
|
||||
dark="/Logo_Dark.png"
|
||||
class="w-1/3 mx-auto my-10"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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
|
||||
variant="outline"
|
||||
color="rose"
|
||||
@click="auth.switchTenant(tenant.id)"
|
||||
>Wählen
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="rose"
|
||||
@click="auth.logout()"
|
||||
>Abmelden</UButton>
|
||||
|
||||
|
||||
>Abmelden
|
||||
</UButton>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</template>
|
||||
40
frontend/stores/calculator.ts
Normal file
40
frontend/stores/calculator.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user