Files
Caramel-CRM/resources/js/pages/Licenses.vue
T

249 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { License } from '@/types'
import { newLicense } from '@/types/index.d'
import { hotkey, toShortISOString } from '@/lib/utils'
import Fuse from 'fuse.js'
import { onMounted, ref, computed } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Head } from '@inertiajs/vue3'
import { Table, TableRow, TableBody, TableCell, TableHead, TableHeader } from '@/components/ui/crm-table'
import { Input } from '@/components/ui/crm-input'
import Button from '@/components/ui/crm-button/Button.vue'
import { ButtonGroup } from '@/components/ui/button-group'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import LicenseService from '@/services/LicenseService'
import { Download, Trash2, Plus, Delete, Search, CheckCircle2, XCircle, CircleQuestionMark, Save } from 'lucide-vue-next'
import { alertStore } from '@/stores/alertStore'
import { toast } from 'vue-sonner'
interface Props {
licenseData: License[]
}
const props = defineProps<Props>()
// Working copy of the licenses shown in the table. All form inputs bind against this array.
const licenses = ref([] as License[])
// Snapshot of the initial server data used to detect whether a license has been changed.
const originalLicenses = ref([] as License[])
// Deep-clone the incoming license list so edits to the UI do not mutate the original props object.
const cloneLicenses = (licenseList: License[]) => JSON.parse(JSON.stringify(licenseList)) as License[]
const searchQuery = ref('')
const searchField = ref()
const alert = alertStore()
onMounted(() => {
licenses.value = cloneLicenses(props.licenseData)
originalLicenses.value = cloneLicenses(licenses.value)
searchField.value = document.getElementById('search')
hotkey('n', createLicense)
})
const fuse = computed(() => {
return new Fuse(licenses.value, {
keys: ['name', 'email', 'product', 'version'],
threshold: 0.3,
})
})
const filteredLicenses = computed(() => {
let filtered = licenses.value
if (searchQuery.value) {
filtered = fuse.value.search(searchQuery.value).map((result) => result.item)
}
return filtered
})
const isDirty = computed(() => {
return licenses.value.some(isLicenseDirty)
})
const isLicenseDirty = (license: License) => {
const originalLicense = originalLicenses.value.find((item) => item.id === license.id)
if (!originalLicense) {
return license.id === 0
}
return JSON.stringify(originalLicense) !== JSON.stringify(license)
}
const createLicense = () => {
LicenseService.createLicense(newLicense()).then((license) => {
if (license) {
licenses.value.unshift(license)
originalLicenses.value = cloneLicenses(licenses.value)
}
})
}
const updateLicenses = async () => {
const dirtyLicenses = licenses.value.filter(isLicenseDirty)
if (dirtyLicenses.length === 0) {
return
}
const response = await LicenseService.updateLicenses(dirtyLicenses)
if (response) {
response.forEach((updatedLicense) => {
var i = licenses.value.findIndex(license => license.id == updatedLicense.id)
licenses.value[i] = updatedLicense
})
}
originalLicenses.value = cloneLicenses(licenses.value)
}
const downloadLicense = (license: License) => {
window.open('/licenses/download/' + license.id)
}
const deleteLicense = (license: License) => {
if (license.id === 0) {
licenses.value = licenses.value.filter((item) => item !== license)
return
}
alert.show(
'Möchtest Du diese Lizenz wirklich löschen?',
null,
{
actionText: 'Löschen',
actionVariant: 'destructive',
onAction: async () => {
LicenseService.deleteLicense(license.id).then(() => {
licenses.value = licenses.value.filter((item) => item.id !== license.id)
originalLicenses.value = cloneLicenses(licenses.value)
}).catch((error) => {
toast.error('Fehler beim Löschen der Lizenz', { duration: 5000, description: error.message })
})
},
},
)
}
const remToPx = (rem: number) => {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
}
</script>
<template>
<Head title="Lizenzen" />
<AppLayout title="Lizenzen">
<!-- Header functions -->
<AppHeader>
<template #left>
</template>
<template #middle>
<!-- Search field -->
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
v-model="searchQuery" />
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
<Search class="size-4 text-muted-foreground" />
</span>
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-0 mr-1">
<Button :size="'sm'" :variant="'ghost'" @click="searchQuery = ''; searchField.focus();">
<Delete class="size-4 text-muted-foreground" />
</Button>
</span>
</template>
<template #right>
<div class="flex gap-2 items-center">
<TooltipProvider>
<!-- Save button -->
<Button variant="action" @click="updateLicenses" v-if="isDirty">
<Save /> Speichern
</Button>
<!-- New button -->
<Tooltip>
<TooltipTrigger>
<Button @click="createLicense">
<Plus /> Neu
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Neue Lizenz anlegen</span>
<KbdGroup class="ml-2"><Kbd>N</Kbd></KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</template>
</AppHeader>
<!-- License table -->
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-1/100 text-center">Gültig</TableHead>
<TableHead class="w-1/100">Perm.</TableHead>
<TableHead class="pl-7">Ablaufdatum</TableHead>
<TableHead class="pl-7">Produkt</TableHead>
<TableHead class="pl-7">Version</TableHead>
<TableHead class="pl-7" colspan="2">Lizenznehmer</TableHead>
<TableHead class="w-1/100 print:hidden"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="license in filteredLicenses" :key="license.id">
<TableCell class="text-center">
<CircleQuestionMark class="size-5 inline-block text-muted-foreground" :size="remToPx(1)"
v-if="license.validationInfo === undefined" />
<CheckCircle2 class="size-5 inline-block text-success" :size="remToPx(1)"
v-else-if="license.validationInfo.isExpired === false" />
<XCircle class="size-5 inline-block text-destructive" :size="remToPx(1)" v-else />
</TableCell>
<TableCell class="text-center">
<input type="checkbox" v-model="license.isPerpetual" />
</TableCell>
<TableCell class="whitespace-nowrap">
<div class="text-center" v-if="license.isPerpetual"></div>
<Input v-else type="date" :modelValue="toShortISOString(license.expirationDate || new Date())"
@update:model-value="(value) => license.expirationDate = value as string"
placeholder="Datum" />
</TableCell>
<TableCell>
<Input type="text" v-model="license.product" placeholder="Produkt Name" />
</TableCell>
<TableCell>
<Input type="text" v-model="license.version" placeholder="1.0.0" />
</TableCell>
<TableCell>
<Input type="text" v-model="license.name" placeholder="Firma" />
</TableCell>
<TableCell>
<Input type="email" v-model="license.email" placeholder="E-Mail" />
</TableCell>
<TableCell class="print:hidden">
<ButtonGroup>
<Button class="size-7" variant="ghost" @click="downloadLicense(license)">
<Download class="text-muted-foreground" />
</Button>
<Button class="size-7" variant="ghost" @click="deleteLicense(license)">
<Trash2 class="text-muted-foreground" />
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
</TableBody>
</Table>
</AppLayout>
</template>