Invoices can be uploaded
This commit is contained in:
56
app/Views/Components/IbanInput.vue
Normal file
56
app/Views/Components/IbanInput.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
const model = defineModel({ type: String, default: '' })
|
||||
|
||||
function onInput(e) {
|
||||
let val = e.target.value
|
||||
|
||||
// alles in Großbuchstaben
|
||||
val = val.toUpperCase()
|
||||
|
||||
// nur Buchstaben, Ziffern und Leerzeichen erlauben
|
||||
val = val.replace(/[^A-Z0-9 ]/g, '')
|
||||
|
||||
// ohne Leerzeichen prüfen
|
||||
const compact = val.replace(/\s+/g, '')
|
||||
|
||||
// max 2 Buchstaben + 20 Ziffern
|
||||
const letters = compact.slice(0, 2).replace(/[^A-Z]/g, '')
|
||||
const digits = compact.slice(2).replace(/[^0-9]/g, '').slice(0, 20)
|
||||
|
||||
// neu zusammensetzen (z. B. alle 4 Zeichen ein Leerzeichen für Lesbarkeit)
|
||||
const formatted = (letters + digits).replace(/(.{4})/g, '$1 ').trim()
|
||||
|
||||
model.value = formatted
|
||||
}
|
||||
|
||||
function onKeypress(e) {
|
||||
const key = e.key
|
||||
|
||||
// immer erlaubt: Leerzeichen
|
||||
if (key === ' ') return
|
||||
|
||||
const compact = model.value.replace(/\s+/g, '')
|
||||
|
||||
if (compact.length < 2) {
|
||||
// in den ersten 2 Stellen nur Buchstaben
|
||||
if (/[A-Za-z]/.test(key)) return
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// danach nur Ziffern bis 20 erlaubt
|
||||
if (/[0-9]/.test(key) && compact.length < 22) return
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
maxlength="27"
|
||||
type="text"
|
||||
:value="model"
|
||||
@input="onInput"
|
||||
@keypress="onKeypress"
|
||||
/>
|
||||
</template>
|
||||
202
app/Views/Components/InfoIcon.vue
Normal file
202
app/Views/Components/InfoIcon.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<!-- InfoIcon.vue -->
|
||||
<template>
|
||||
<span
|
||||
class="info-icon-wrapper"
|
||||
role="button"
|
||||
:aria-label="ariaLabel"
|
||||
tabindex="0"
|
||||
@mouseenter="open"
|
||||
@mouseleave="close"
|
||||
@focus="open"
|
||||
@blur="close"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<slot name="icon">
|
||||
<!-- default info SVG -->
|
||||
<svg class="info-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.08"></circle>
|
||||
<path d="M11 17h2v-6h-2v6zm0-8h2V7h-2v2z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</slot>
|
||||
|
||||
<transition name="fade-scale">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="tooltip"
|
||||
:class="positionClass"
|
||||
role="tooltip"
|
||||
:id="tooltipId"
|
||||
>
|
||||
<div class="tooltip-inner" v-html="text"></div>
|
||||
<!-- small arrow -->
|
||||
<div class="tooltip-arrow" aria-hidden="true"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true }, // Tooltiptext (HTML erlaubt)
|
||||
position: { type: String, default: 'top' }, // top | right | bottom | left
|
||||
delay: { type: Number, default: 80 }, // ms - delay für Öffnen/Schließen (klein)
|
||||
ariaLabel: { type: String, default: 'Info' }, // aria-label für das Icon (z.B. "Mehr Informationen")
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
let openTimer = null
|
||||
let closeTimer = null
|
||||
|
||||
const tooltipId = `info-icon-tooltip-${Math.round(Math.random()*1e6)}`
|
||||
|
||||
const positionClass = computed(() => {
|
||||
const p = props.position
|
||||
return `pos-${p}`
|
||||
})
|
||||
|
||||
function open() {
|
||||
clearTimeout(closeTimer)
|
||||
openTimer = setTimeout(() => (visible.value = true), props.delay)
|
||||
}
|
||||
|
||||
function close() {
|
||||
clearTimeout(openTimer)
|
||||
closeTimer = setTimeout(() => (visible.value = false), props.delay)
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
visible.value = false
|
||||
e.stopPropagation()
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
// toggle on Enter / Space
|
||||
e.preventDefault()
|
||||
visible.value = !visible.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-icon-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
cursor: help;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* focus ring */
|
||||
.info-icon-wrapper:focus {
|
||||
box-shadow: 0 0 0 4px rgba(50,115,220,0.12);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* SVG sizing */
|
||||
.info-icon {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Tooltip baseline */
|
||||
/* Tooltip baseline */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
min-width: 180px; /* optional, sorgt für nicht zu kleinen Tooltip */
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
padding: 8px 10px;
|
||||
background: #59a3da;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
white-space: normal; /* erlaubt Umbruch */
|
||||
word-break: break-word; /* lange Wörter umbrechen */
|
||||
}
|
||||
|
||||
/* Arrow */
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transform: rotate(45deg);
|
||||
background: inherit;
|
||||
box-shadow: inherit;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* Positions */
|
||||
.pos-top {
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pos-top .tooltip-arrow {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-bottom {
|
||||
top: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pos-bottom .tooltip-arrow {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-left {
|
||||
right: calc(100% + 8px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pos-left .tooltip-arrow {
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-right {
|
||||
left: calc(100% + 8px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pos-right .tooltip-arrow {
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
/* inner tooltip text styling */
|
||||
.tooltip-inner {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* enter/leave animation */
|
||||
.fade-scale-enter-active,
|
||||
.fade-scale-leave-active {
|
||||
transition: opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
.fade-scale-enter-from,
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.fade-scale-enter-to,
|
||||
.fade-scale-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
17
app/Views/Components/NumericInput.vue
Normal file
17
app/Views/Components/NumericInput.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- NumericInput.vue -->
|
||||
<script setup>
|
||||
const model = defineModel() // bindet v-model automatisch
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:value="model"
|
||||
@input="model = $event.target.value.replace(/[^0-9]/g, '')"
|
||||
@keypress="($event) => {
|
||||
if (!/[0-9]/.test($event.key)) {
|
||||
$event.preventDefault()
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
29
app/Views/Components/TextResource.vue
Normal file
29
app/Views/Components/TextResource.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
textName: { type: String},
|
||||
belongsTo: { type: String},
|
||||
})
|
||||
|
||||
const contentData = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-text-resource/' + props.textName);
|
||||
const data = await response.json();
|
||||
Object.assign(contentData, data);
|
||||
console.log(contentData)
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label :for="props.belongsTo">{{contentData.content}}</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user