mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
289 lines
13 KiB
Vue
289 lines
13 KiB
Vue
<script lang="ts" setup>
|
|
import { InfoIcon, Upload, Trash2, Lock } from 'lucide-vue-next';
|
|
import { toast } from 'vue-sonner';
|
|
|
|
|
|
definePageMeta({ layout: 'sidebar' });
|
|
|
|
const theme = ref<string>('');
|
|
const reportType = ref<string>('');
|
|
const loading = ref<boolean>(false);
|
|
const currentImageB64 = ref<string>('');
|
|
|
|
const pdfUrl = ref<string>('');
|
|
|
|
const snapshotStore = useSnapshotStore();
|
|
const projectStore = useProjectStore();
|
|
const domainStore = useDomainStore();
|
|
const premium = usePremiumStore();
|
|
|
|
const domain = ref<string>();
|
|
|
|
const canGenerate = computed(() => {
|
|
if (reportType.value === 'custom') return theme.value && reportType.value;
|
|
if (reportType.value === 'advanced') return domain.value && reportType.value;
|
|
return reportType.value;
|
|
})
|
|
|
|
|
|
async function getInputLogo(e: any) {
|
|
if (!e.target.files) return;
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const b64 = await new Promise<string>(resolve => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = function () {
|
|
const base64 = reader.result;
|
|
if (!base64) throw Error('Error reading image');
|
|
resolve(base64 as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
})
|
|
|
|
currentImageB64.value = b64;
|
|
|
|
|
|
}
|
|
|
|
async function generateReport() {
|
|
loading.value = true;
|
|
|
|
if (pdfUrl.value !== '') {
|
|
URL.revokeObjectURL(pdfUrl.value);
|
|
pdfUrl.value = '';
|
|
}
|
|
const res = await useAuthFetchSync<any>(reportType.value === 'advanced' ? `/api/project/generate_pdf_adv?domain=${domain.value}` : `/api/project/generate_pdf?theme=${theme.value}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: {
|
|
snapshotName: snapshotStore.activeSnapshot?.name ?? 'NO_NAME',
|
|
customLogo: currentImageB64.value
|
|
},
|
|
responseType: 'blob'
|
|
});
|
|
|
|
const blob = new Blob([res], { type: 'application/pdf' });
|
|
const url = URL.createObjectURL(blob);
|
|
toast.info('Report', { position: 'top-right', description: `Report succesfully generated! (${reportType.value} report)` });
|
|
|
|
pdfUrl.value = url
|
|
loading.value = false;
|
|
reportType.value = '';
|
|
theme.value = '';
|
|
currentImageB64.value = '';
|
|
}
|
|
|
|
async function resetPdfUrl() {
|
|
URL.revokeObjectURL(pdfUrl.value);
|
|
pdfUrl.value = '';
|
|
}
|
|
|
|
async function downloadPdf() {
|
|
const a = document.createElement('a');
|
|
a.href = pdfUrl.value;
|
|
a.download = `Litlyx_Report${reportType.value === 'advanced' ? '_Advanced' : ''}.pdf`;
|
|
a.click();
|
|
resetPdfUrl()
|
|
toast.success('Success', { position: 'top-right', description: 'Report succesfully downloaded' });
|
|
}
|
|
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
const triggerFileInput = () => {
|
|
fileInput.value?.click()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Unauthorized v-if="!projectStore.isOwner" authorization="Guest user limitation Reports">
|
|
</Unauthorized>
|
|
<div v-else class="grid grid-cols-1 poppins">
|
|
<Card v-if="!pdfUrl" class="w-full justify-self-center">
|
|
<CardHeader>
|
|
<CardTitle class="flex gap-2">
|
|
Generate a report
|
|
<Tooltip>
|
|
<TooltipTrigger as-child>
|
|
<InfoIcon class="size-4"></InfoIcon>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
The report follows the current Timeframe
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</CardTitle>
|
|
<CardDescription>Generate a report of your workspace</CardDescription>
|
|
<CardAction>
|
|
<div>
|
|
<div v-if="!loading" class="flex items-center gap-2">
|
|
<Button @click="generateReport()" :disabled="!canGenerate">Generate report</Button>
|
|
</div>
|
|
<Button v-else disabled>
|
|
<Loader class="!size-4"></Loader> Generating...
|
|
</Button>
|
|
</div>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent class="flex justify-center lg:justify-start">
|
|
<div class="gap-4"
|
|
:class="reportType === 'custom' ? 'grid grid-cols-1 lg:grid-cols-2' : 'flex flex-col'">
|
|
<div class="flex flex-col gap-4">
|
|
<Label>Report type </Label>
|
|
<Select v-model="reportType" :disabled="loading">
|
|
<SelectTrigger class="w-[20rem]">
|
|
<SelectValue placeholder="Report types" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel>Types</SelectLabel>
|
|
<SelectItem value="easy">
|
|
Easy report
|
|
</SelectItem>
|
|
|
|
|
|
|
|
<SelectItem value="locked" disabled as-child
|
|
v-if="!premium.planInfo?.features.customizable_report">
|
|
<NuxtLink to="/plans"
|
|
class="flex items-center gap-2 pl-4 py-2 rounded-md text-violet-200 bg-violet-500 hover:!bg-violet-600/80 dark:bg-violet-500/20 hover:dark:!bg-violet-500/30">
|
|
<Lock class="size-4 text-yellow-500" /> Custom report
|
|
</NuxtLink>
|
|
</SelectItem>
|
|
<SelectItem value="custom" v-else>
|
|
Custom Report
|
|
</SelectItem>
|
|
|
|
<SelectItem value="locked" disabled as-child
|
|
v-if="!premium.planInfo?.features.customizable_report">
|
|
<NuxtLink to="/plans"
|
|
class="flex items-center gap-2 pl-4 py-2 rounded-md text-violet-200 bg-violet-500 hover:!bg-violet-600/80 dark:bg-violet-500/20 hover:dark:!bg-violet-500/30">
|
|
<Lock class="size-4 text-yellow-500" /> Advanced report
|
|
</NuxtLink>
|
|
</SelectItem>
|
|
<SelectItem value="advanced" v-else>
|
|
Advanced Report
|
|
</SelectItem>
|
|
|
|
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<div v-if="reportType === 'custom'" class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<Label>Report variant</Label>
|
|
<Select v-model="theme" :disabled="loading">
|
|
<SelectTrigger class="w-[20rem]">
|
|
<SelectValue placeholder="Report variants" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectItem value="black">
|
|
Black
|
|
</SelectItem>
|
|
<SelectItem value="white">
|
|
White
|
|
</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<!-- <div class="flex flex-col gap-2">
|
|
<Label>Custom Logo</Label>
|
|
<Input @input="getInputLogo" class="w-[20rem]" type="file"></Input>
|
|
</div> -->
|
|
</div>
|
|
</div>
|
|
<div v-if="reportType === 'custom'">
|
|
<div class="flex flex-col gap-4">
|
|
<Label>Your logo</Label>
|
|
<div
|
|
class="group bg-muted h-80 w-80 p-4 rounded-lg cursor-pointer hover:bg-muted/80 duration-300">
|
|
|
|
<input type="file" ref="fileInput" class="hidden" accept="image/*"
|
|
@input="getInputLogo" />
|
|
<div v-if="currentImageB64 === ''" @click="triggerFileInput"
|
|
class="flex justify-center items-center border-4 border-muted-foreground border-dashed h-full w-full rounded-sm group-hover:animate-pulse">
|
|
<Upload
|
|
class="size-8 group-hover:size-9 transition-all duration-300 text-muted-foreground" />
|
|
</div>
|
|
<div v-else class="relative">
|
|
<Button :disabled="loading" class="absolute top-0 right-0"
|
|
@click="currentImageB64 = ''" variant="ghost" size="icon">
|
|
<Trash2 class="size-4" />
|
|
</Button>
|
|
<img :src="currentImageB64" alt="Preview"
|
|
class="h-full w-full object-cover rounded" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div v-if="reportType === 'advanced'">
|
|
|
|
<div class="flex flex-col gap-4">
|
|
|
|
<Label>Select domain</Label>
|
|
|
|
<Select v-model="domain">
|
|
<SelectTrigger class="w-[20rem]">
|
|
<SelectValue placeholder="Domain" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem v-for="domain of domainStore.domains.slice(1)" :value="domain._id">
|
|
{{ domain.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
|
|
<Label>Your logo</Label>
|
|
<div
|
|
class="group bg-muted h-80 w-80 p-4 rounded-lg cursor-pointer hover:bg-muted/80 duration-300">
|
|
|
|
<input type="file" ref="fileInput" class="hidden" accept="image/*"
|
|
@input="getInputLogo" />
|
|
<div v-if="currentImageB64 === ''" @click="triggerFileInput"
|
|
class="flex justify-center items-center border-4 border-muted-foreground border-dashed h-full w-full rounded-sm group-hover:animate-pulse">
|
|
<Upload
|
|
class="size-8 group-hover:size-9 transition-all duration-300 text-muted-foreground" />
|
|
</div>
|
|
<div v-else class="relative">
|
|
<Button :disabled="loading" class="absolute top-0 right-0"
|
|
@click="currentImageB64 = ''" variant="ghost" size="icon">
|
|
<Trash2 class="size-4" />
|
|
</Button>
|
|
<img :src="currentImageB64" alt="Preview"
|
|
class="h-full w-full object-cover rounded" />
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card v-if="pdfUrl">
|
|
<CardHeader class="flex justify-between">
|
|
<div class="flex flex-col gap-2">
|
|
<CardTitle>
|
|
Report Preview
|
|
</CardTitle>
|
|
<CardDescription>Your report is now ready</CardDescription>
|
|
</div>
|
|
<div class="flex gap-4">
|
|
<Button variant="outline" @click="resetPdfUrl()">New Report</Button>
|
|
<Button @click="downloadPdf">Download</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<iframe :src="pdfUrl" type="application/pdf" class="w-full h-[50rem]" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</template> |