-
+
diff --git a/resources/js/components/ui/table/TableHead.vue b/resources/js/components/ui/table/TableHead.vue
index 06d0022..ddf635c 100644
--- a/resources/js/components/ui/table/TableHead.vue
+++ b/resources/js/components/ui/table/TableHead.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
- |
+ |
|
diff --git a/resources/js/components/ui/table/TableHeader.vue b/resources/js/components/ui/table/TableHeader.vue
index b4ab5cf..66e1e60 100644
--- a/resources/js/components/ui/table/TableHeader.vue
+++ b/resources/js/components/ui/table/TableHeader.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
-
+
diff --git a/resources/js/components/ui/table/TableRow.vue b/resources/js/components/ui/table/TableRow.vue
index 7c29a3a..036db30 100644
--- a/resources/js/components/ui/table/TableRow.vue
+++ b/resources/js/components/ui/table/TableRow.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
-
+
diff --git a/resources/js/layouts/AppLayout.vue b/resources/js/layouts/AppLayout.vue
index f11f2d4..770fc26 100644
--- a/resources/js/layouts/AppLayout.vue
+++ b/resources/js/layouts/AppLayout.vue
@@ -6,7 +6,7 @@ import 'vue-sonner/style.css'
import { Toaster } from 'vue-sonner'
import { Info, CircleAlert, CircleCheck, LoaderCircle, Ban } from "lucide-vue-next"
import { Button } from '@/components/ui/crm-button'
-import { SidebarProvider } from '@/components/ui/sidebar';
+import { SidebarProvider } from '@/components/ui/crm-sidebar';
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'
import { alertStore } from '@/stores/alertStore';
import { webcronStore } from '@/stores/webcronStore';
@@ -70,12 +70,12 @@ onMounted(() => {
-
+
-
+
@@ -115,12 +115,5 @@ onMounted(() => {
background-color: transparent;
}
- main {
- margin: 0;
- background-color: transparent;
- border-radius: 0;
- box-shadow: none;
- outline: none;
- }
}
\ No newline at end of file
diff --git a/resources/js/layouts/auth/AuthSplitLayout.vue b/resources/js/layouts/auth/AuthSplitLayout.vue
index e24e5a1..a4246a7 100644
--- a/resources/js/layouts/auth/AuthSplitLayout.vue
+++ b/resources/js/layouts/auth/AuthSplitLayout.vue
@@ -1,6 +1,5 @@
-
-
-
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts
index 302a6b1..09d8a2c 100644
--- a/resources/js/lib/utils.ts
+++ b/resources/js/lib/utils.ts
@@ -3,10 +3,40 @@ import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { toast } from 'vue-sonner'
+
+// -----------------------------------------------------------------------------
+// Attributes, classes
+// -----------------------------------------------------------------------------
+
+// #region Clöasses
+
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+const randomColors = [
+ 'bg-rose-200 dark:bg-rose-700',
+ 'bg-lime-200 dark:bg-lime-700',
+ 'bg-orange-200 dark:bg-orange-700',
+ 'bg-sky-200 dark:bg-sky-700',
+ 'bg-amber-200 dark:bg-amber-700',
+ 'bg-blue-200 dark:bg-blue-700',
+ 'bg-purple-200 dark:bg-purple-700'
+]
+
+export function bgColorForString(input: string): string {
+ let charCodeSum = 0;
+ for (let i = 0; i < input.length; i++) {
+ charCodeSum += input.charCodeAt(i)
+ }
+ return randomColors[charCodeSum % randomColors.length];
+}
+
+
+// -----------------------------------------------------------------------------
+// URLs
+// -----------------------------------------------------------------------------
+
export function urlIsActive(urlToCheck: NonNullable , currentUrl: string) {
return currentUrl.indexOf(toUrl(urlToCheck)) >= 0
}
@@ -16,6 +46,25 @@ export function toUrl(href: NonNullable) {
}
+// -----------------------------------------------------------------------------
+// Number helpers and formatting
+// -----------------------------------------------------------------------------
+
+export function toFixedRounded(num: number, precision: number): number {
+ return Number((Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision)).toFixed(precision))
+}
+
+export function randomInt(max: number) {
+ return Math.floor(Math.random() * max);
+}
+
+export function isNumeric(value: any) {
+ return !isNaN(parseFloat(value))
+}
+
+
+// Currency
+
const currencyFormatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
@@ -39,86 +88,122 @@ export function toRoundedCurrency(value: number | undefined) {
return roundedCurrencyFormatter.format(toFixedRounded(value, 0));
}
-export function toFixedRounded(num: number, precision: number): number {
- return Number((Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision)).toFixed(precision))
+
+// Dates
+
+const locale = "de-De"
+const closeDaysThreshhold = 2
+const rtf = new Intl.RelativeTimeFormat(locale, {
+ numeric: "auto", // "always" | "auto"
+ style: "long", // "long" | "short" | "narrow"
+});
+
+export function isToday(value: string | Date): boolean {
+ return daysFromNow(value) === 0
+}
+
+export function isBefore(value: string | Date, comparison: string | Date): boolean {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ let comparisonDate = (typeof comparison === 'string') ? new Date(comparison) : comparison
+ return date.getTime() < comparisonDate.getTime()
+}
+
+export function isAfter(value: string | Date, comparison: string | Date) {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ let comparisonDate = (typeof comparison === 'string') ? new Date(comparison) : comparison
+ return date.getTime() > comparisonDate.getTime()
+}
+
+export function millisFromNow(value: string | Date): number {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ return date.getTime() - new Date().getTime()
+}
+
+export function minutesFromNow(value: string | Date): number {
+ return Math.round(millisFromNow(value) / (1000 * 60))
+}
+
+export function hoursFromNow(value: string | Date): number {
+ return Math.round(millisFromNow(value) / (1000 * 60 * 60))
+}
+
+export function daysFromNow(value: string | Date): number {
+ return Math.round(millisFromNow(value) / (1000 * 60 * 60 * 24))
+}
+
+export function isThisHour(value: Date | string): boolean {
+ if (!isToday(value)) return false
+ return Math.abs(hoursFromNow(value)) <= 1
+}
+
+export function isCloseDays(value: Date | string): boolean {
+ return Math.abs(daysFromNow(value)) <= closeDaysThreshhold
+}
+
+export function isSoon(value: Date | string): boolean {
+ const days = daysFromNow(value)
+ return days > 0 && days <= closeDaysThreshhold
+}
+
+export function isThisYear(value: Date | string): boolean {
+ return new Date().getFullYear() == new Date(value).getFullYear()
+}
+
+export function toDuration(value: string | Date): string {
+ if (isThisHour(value)) return rtf.format(minutesFromNow(value), 'minute')
+ else if (isCloseDays(value)) return rtf.format(daysFromNow(value), 'day')
+ else if (isThisYear(value)) return toShortLocalDate(value)
+ else return toLocalDate(value)
}
export function toLocalDate(value: string | Date) {
- const date = new Date(value);
- return date.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' });
- // return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
- // return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' });
+ let date = (typeof value === 'string') ? new Date(value) : value
+ return date.toLocaleDateString(locale, { day: '2-digit', month: 'short', year: 'numeric' });
}
-export function toShortISOString(date: Date | string) {
- let tempDate = (typeof date === 'string') ? new Date(date) : date
- return tempDate.toISOString().split('T')[0]
+export function toShortLocalDate(value: string | Date) {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ return date.toLocaleDateString(locale, { day: 'numeric', month: 'short' });
}
-export function calcDueDate(date: Date, daysAfterInvoice: number): Date {
+export function toShortISOString(value: Date | string) {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ return date.toISOString().split('T')[0]
+}
+
+export function toDatetimeLocal(value: Date | string) {
+ let date = (typeof value === 'string') ? new Date(value) : value
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
+}
+
+export function calcDueDate(date: Date | string, daysAfterInvoice: number): Date {
let dueDate = new Date(date);
- dueDate.setDate(date.getDate() + daysAfterInvoice);
+ dueDate.setDate(dueDate.getDate() + daysAfterInvoice);
return dueDate;
}
-export function randomInt(max: number) {
- return Math.floor(Math.random() * max);
+export function randomDate(): Date {
+ let date = new Date()
+ date.setDate(date.getDate() - Math.random() * 20)
+ return date
}
-const randomColors = [
- 'bg-rose-200 dark:bg-rose-700',
- 'bg-lime-200 dark:bg-lime-700',
- 'bg-orange-200 dark:bg-orange-700',
- 'bg-sky-200 dark:bg-sky-700',
- 'bg-amber-200 dark:bg-amber-700',
- 'bg-blue-200 dark:bg-blue-700',
- 'bg-purple-200 dark:bg-purple-700'
-]
-export function bgColorForString(input: string): string {
- let charCodeSum = 0;
- for (let i = 0; i < input.length; i++) {
- charCodeSum += input.charCodeAt(i)
- }
- return randomColors[charCodeSum % randomColors.length];
-}
+
+// -----------------------------------------------------------------------------
+// Text and strings
+// -----------------------------------------------------------------------------
export function clearSelection() {
window.getSelection()?.removeAllRanges();
};
-const promise = () => new Promise((resolve) => setTimeout(resolve, 2000))
-export function testToast() {
- let delay = 150
- toast('Toast', {
- description: "Description bjksad fkjlhsd fkjhsdf jkshdf jkashdf adskljhf ", action: {
- label: 'Undo',
- onClick: () => console.log('Undo')
- }
- })
- setTimeout(() => {
- toast.info('Info', {
- action: {
- label: 'Undo',
- onClick: () => console.log('Undo')
- }
- })
- }, delay * 1)
- setTimeout(() => { toast.success('Success', { description: "description" }) }, delay * 2)
- setTimeout(() => { toast.warning('Warning', { description: "description" }) }, delay * 3)
- setTimeout(() => { toast.error('Error', { description: "description" }) }, delay * 4)
- setTimeout(() => {
- toast.promise(promise, {
- loading: 'Loading...',
- success: (data) => {
- return `${data.name} toast has been added`;
- },
- error: (data: any) => 'Error',
- })
- }, delay * 5)
-
-}
-
export function loremIpsum(numWords: number): string {
let text = `Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
@@ -130,15 +215,9 @@ Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming
return text.split(" ").splice(0, numWords).join(" ");
}
-export function randomDate(): Date {
- let d = new Date()
- d.setDate(d.getDate() - Math.random() * 20)
- return d
-}
-
// -----------------------------------------------------------------------------
-// Hotkeys
+// Keyboard shortcuts
// -----------------------------------------------------------------------------
// Füge diese Typdefinitionen am Anfang der Datei hinzu
@@ -209,11 +288,6 @@ export function hotkey(
.map(key => normalizeKeyName(key))
.join('+')
- // Überprüfen, ob der Shortcut bereits registriert ist
- if (registeredHotkeys.has(normalizedCombination)) {
- console.warn(`Hotkey '${keyCombination}' is already registered. Overwriting existing hotkey.`)
- }
-
// Speichern der Hotkey-Informationen
registeredHotkeys.set(normalizedCombination, {
callback,
@@ -319,4 +393,41 @@ window.addEventListener('keydown', handleKeyboardEvent);
// Füge die Funktion zum Entfernen aller Hotkeys hinzu
export function removeAllHotkeys() {
registeredHotkeys.clear()
+}
+
+
+// -----------------------------------------------------------------------------
+// Misc
+// -----------------------------------------------------------------------------
+
+const promise = () => new Promise((resolve) => setTimeout(resolve, 2000))
+export function testToast() {
+ let delay = 150
+ toast('Toast', {
+ description: "Description bjksad fkjlhsd fkjhsdf jkshdf jkashdf adskljhf ", action: {
+ label: 'Undo',
+ onClick: () => console.log('Undo')
+ }
+ })
+ setTimeout(() => {
+ toast.info('Info', {
+ action: {
+ label: 'Undo',
+ onClick: () => console.log('Undo')
+ }
+ })
+ }, delay * 1)
+ setTimeout(() => { toast.success('Success', { description: "description" }) }, delay * 2)
+ setTimeout(() => { toast.warning('Warning', { description: "description" }) }, delay * 3)
+ setTimeout(() => { toast.error('Error', { description: "description" }) }, delay * 4)
+ setTimeout(() => {
+ toast.promise(promise, {
+ loading: 'Loading...',
+ success: (data) => {
+ return `${data.name} toast has been added`;
+ },
+ error: (data: any) => 'Error',
+ })
+ }, delay * 5)
+
}
\ No newline at end of file
diff --git a/resources/js/pages/Achievements.vue b/resources/js/pages/Achievements.vue
index b69da46..1a6da9b 100644
--- a/resources/js/pages/Achievements.vue
+++ b/resources/js/pages/Achievements.vue
@@ -1,4 +1,5 @@
@@ -7,8 +8,7 @@ import AppLayout from '@/layouts/AppLayout.vue';
- Kategorie
- 3 von 10 freigeschaltet
+
diff --git a/resources/js/pages/CRM.vue b/resources/js/pages/CRM.vue
deleted file mode 100644
index dbf2313..0000000
--- a/resources/js/pages/CRM.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/resources/js/pages/Customers.vue b/resources/js/pages/Customers.vue
index ced8626..083b0a5 100644
--- a/resources/js/pages/Customers.vue
+++ b/resources/js/pages/Customers.vue
@@ -5,7 +5,7 @@ import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Address, Customer } from '@/types'
import { newCustomer } from '@/types/index.d'
-import { bgColorForString } from '@/lib/utils'
+import { hotkey, bgColorForString } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Input } from '@/components/ui/crm-input'
import { Button } from '@/components/ui/crm-button'
@@ -14,6 +14,8 @@ import { Delete, Globe, House, LayoutGrid, Mail, Phone, Plus, Rows3, Search, Sma
import Fuse from 'fuse.js';
import { getInitials } from '@/composables/useInitials';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogClose } from "@/components/ui/dialog"
+import DialogCloseButton from "@/components/DialogCloseButton/DialogCloseButton.vue"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { TooltipArrow } from 'reka-ui'
import CustomerDialog from '@/components/CustomerDialog.vue'
@@ -32,11 +34,15 @@ const searchQuery = ref('')
const searchField = ref()
const activeCustomer = ref (null)
const detailDialogOpen = ref(false)
+const phoneNumber = ref('')
onMounted(() => {
customersData.value = props.customersData.toSorted((a, b) => (a.companyName ?? '').localeCompare(b.companyName ?? ''));
searchField.value = document.getElementById('search')
searchField.value.focus()
+
+ // Register hotkeys
+ hotkey('mod+f', () => { searchField.value.focus() })
})
watch(activeCustomer, () => {
@@ -113,8 +119,32 @@ const browse = (url: string, event: Event) => {
}
}
+const showPhoneNumber = (number: string, event: Event | null) => {
+ if (event) {
+ event.preventDefault()
+ event.stopPropagation();
+ event.returnValue = true
+ }
+
+ phoneNumber.value = number
+}
+
+const closePhoneNumberDisplay = (event: Event | null) => {
+ if (event) {
+ event.preventDefault()
+ event.stopPropagation();
+ event.returnValue = true
+ }
+
+ phoneNumber.value = ''
+}
+
const call = (number: string, event: Event) => {
- event.stopPropagation();
+ if (event) {
+ event.preventDefault()
+ event.stopPropagation();
+ event.returnValue = true
+ }
window.open('tel:' + number, '_self')
}
@@ -129,10 +159,10 @@ const call = (number: string, event: Event) => {
@@ -143,11 +173,11 @@ const call = (number: string, event: Event) => {
-
+
@@ -157,7 +187,7 @@ const call = (number: string, event: Event) => {
- |