Added license management module
This commit is contained in:
@@ -3,12 +3,12 @@ import NavFooter from '@/components/NavFooter.vue';
|
||||
import NavMain from '@/components/NavMain.vue';
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger } from '@/components/ui/crm-sidebar';
|
||||
import { useSidebar } from '@/components/ui/crm-sidebar/utils'
|
||||
import { dashboard, pipeline, offers, invoices, newInvoice, products, timesheets, customers, leads, achievements } from '@/routes';
|
||||
import { dashboard, pipeline, licenses, offers, invoices, newInvoice, products, timesheets, customers, leads, achievements } from '@/routes';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
||||
import { type NavGroup } from '@/types';
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { Kanban, Euro, Trophy, Calculator, Timer, ContactRound, Headset, Plus, Package } from 'lucide-vue-next';
|
||||
import { Kanban, Euro, Trophy, Calculator, Timer, ContactRound, Headset, Plus, Package, FileBadge } from 'lucide-vue-next';
|
||||
import AppLogo from './AppLogo.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -66,6 +66,11 @@ const mainNavGroups: NavGroup[] = [
|
||||
href: products(),
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Lizenzen',
|
||||
href: licenses(),
|
||||
icon: FileBadge,
|
||||
},
|
||||
{
|
||||
title: 'Zeiterfassung',
|
||||
href: timesheets(),
|
||||
|
||||
@@ -14,6 +14,8 @@ export const badgeVariants = cva(
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
success:
|
||||
"border-transparent bg-success text-success-foreground [a&]:hover:bg-success/90 focus-visible:ring-success/20 dark:focus-visible:ring-success/40 dark:bg-success/60",
|
||||
warning:
|
||||
"border-transparent bg-warning text-warning-foreground [a&]:hover:bg-warning/90 focus-visible:ring-warning/20 dark:focus-visible:ring-warning/40 dark:bg-warning/60",
|
||||
outline:
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { License } from '@/types'
|
||||
import { toLocalDate } 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, TableFooter, TableHead, TableHeader } from '@/components/ui/crm-table'
|
||||
import { Input } from '@/components/ui/crm-input'
|
||||
import { Badge } from '@/components/ui/crm-badge'
|
||||
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 { Check, Download, Trash2, Plus, Delete, Search } from 'lucide-vue-next'
|
||||
import { alertStore } from "@/stores/alertStore"
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
interface Props {
|
||||
licenseData: License[]
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const licenses = ref([] as License[])
|
||||
const searchQuery = ref('')
|
||||
const searchField = ref()
|
||||
const alert = alertStore()
|
||||
|
||||
onMounted(() => {
|
||||
licenses.value = props.licenseData
|
||||
})
|
||||
|
||||
const fuse = computed(() => {
|
||||
return new Fuse(props.licenseData, {
|
||||
keys: ['name', 'email', 'product', 'version'],
|
||||
threshold: 0.3
|
||||
})
|
||||
})
|
||||
|
||||
const filteredLicenses = computed(() => {
|
||||
|
||||
let licenses = props.licenseData
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value) {
|
||||
licenses = fuse.value.search(searchQuery.value).map(result => result.item)
|
||||
}
|
||||
|
||||
return licenses
|
||||
})
|
||||
|
||||
const downloadLicense = (id: number) => {
|
||||
window.open('/licenses/download/' + id)
|
||||
}
|
||||
|
||||
const deleteLicense = (id: number) => {
|
||||
alert.show(
|
||||
"Möchtest Du diese Lizenz wirklich löschen?",
|
||||
null,
|
||||
{
|
||||
actionText: "Löschen",
|
||||
actionVariant: "destructive",
|
||||
onAction: async () => {
|
||||
LicenseService.deleteLicense(id).then(() => {
|
||||
licenses.value = licenses.value.filter(license => license.id != id)
|
||||
}).catch((error) => {
|
||||
toast.error('Fehler beim Löschen der Lizenz', { duration: 5000, description: error.message })
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
</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>
|
||||
<!-- New button -->
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button @click="">
|
||||
<Plus />
|
||||
Neu
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>Neue Lizenz anlegen</span>
|
||||
<KbdGroup class="ml-2">
|
||||
<Kbd class="visible-mac">⌘ N</Kbd>
|
||||
<Kbd class="visible-pc">Ctrl N</Kbd>
|
||||
</KbdGroup>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
</AppHeader>
|
||||
|
||||
<!-- License table -->
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-1/100 text-center">Gültig bis</TableHead>
|
||||
<TableHead>Produkt</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead 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">
|
||||
<!-- <Check class="text-success inline-block" /> -->
|
||||
<Badge v-if="license.isPerpetual" variant="secondary">
|
||||
Permanent
|
||||
</Badge>
|
||||
<Badge v-else-if="Math.random() > 0.5" variant="success">
|
||||
{{ toLocalDate(license.expirationDate || '') }}
|
||||
</Badge>
|
||||
<Badge v-else variant="destructive">
|
||||
{{ toLocalDate(license.expirationDate || '') }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{{ license.product }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{{ license.version }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{{ license.name }}<br />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<a :href="'mailto:' + license.email + '?subject=Ihre ' + license.product + '-Lizenz'"
|
||||
class="text-muted-foreground text-sm">{{ license.email
|
||||
}}</a>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="print:hidden">
|
||||
<ButtonGroup>
|
||||
<Button size="icon" variant="ghost" @click="downloadLicense(license.id)">
|
||||
<Download class="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" @click="deleteLicense(license.id)">
|
||||
<Trash2 class="text-muted-foreground" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { License } from '@/types';
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
const API_URL = '/api/licenses';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Fetches all licenses
|
||||
* @returns Promise<License[] | null>
|
||||
*/
|
||||
async getLicenses(): Promise<License[] | null> {
|
||||
try {
|
||||
const response = await axios.get(API_URL);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Abrufen der Lizenzen', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches a single license by ID
|
||||
* @param id - The ID of the license to fetch
|
||||
* @returns Promise<License | null>
|
||||
*/
|
||||
async getLicense(id: number): Promise<License | null> {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Abrufen der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new license
|
||||
* @param license - The license data to create
|
||||
* @returns Promise<License | null>
|
||||
*/
|
||||
async createLicense(license: License): Promise<License | null> {
|
||||
try {
|
||||
const response = await axios.post(API_URL, license);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Erstellen der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing license
|
||||
* @param license - The license data to update
|
||||
* @returns Promise<License | null>
|
||||
*/
|
||||
async updateLicense(license: License): Promise<License | null> {
|
||||
try {
|
||||
const response = await axios.put(`${API_URL}/${license.id}`, license);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Aktualisieren der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a license by ID
|
||||
* @param id - The ID of the license to delete
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async deleteLicense(id: number): Promise<boolean> {
|
||||
try {
|
||||
await axios.delete(`${API_URL}/${id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Löschen der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Downloads a license as a JSON file
|
||||
* @param id - The ID of the license to download
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async downloadLicense(id: number): Promise<void> {
|
||||
try {
|
||||
const response = await axios.get(`/licenses/download/${id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `license_${id}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Herunterladen der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a license
|
||||
* @param licenseId - The ID of the license to validate
|
||||
* @returns Promise<{ is_valid: boolean, is_signature_valid: boolean, is_expired: boolean } | null>
|
||||
*/
|
||||
async validateLicense(licenseId: number): Promise<{ is_valid: boolean, is_signature_valid: boolean, is_expired: boolean } | null> {
|
||||
try {
|
||||
const response = await axios.post('/api/licenses/validate', { license_id: licenseId });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Validieren der Lizenz', { description: (error as AxiosError).message });
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
Vendored
+28
-1
@@ -289,6 +289,34 @@ export function newBillingData() {
|
||||
}
|
||||
}
|
||||
|
||||
export interface License {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
product: string;
|
||||
version: string;
|
||||
isPerpetual: boolean;
|
||||
expirationDate: string | null;
|
||||
signature: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function newLicense(): License {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
email: '',
|
||||
product: '',
|
||||
version: '',
|
||||
isPerpetual: false,
|
||||
expirationDate: null,
|
||||
signature: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export interface LineItem {
|
||||
id: number,
|
||||
invoiceId: number,
|
||||
@@ -411,7 +439,6 @@ export function newProduct(): Product {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pipeline item used by CRM pipeline
|
||||
export interface PipelineItem {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user