Files
solidtime/resources/js/Pages/Teams/Partials/ImportData.vue
Gregor Vostrak e78a551098 refactor to shadcn components, dynamically load extension frontend
add jetstream permissions, add dynamic inertia module loading, add shadcn components, change modals and dropdowns to shadcn dismissable layer,
2025-04-23 14:33:32 +02:00

260 lines
9.7 KiB
Vue

<script setup lang="ts">
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { computed, onMounted, ref } from 'vue';
import { useNotificationsStore } from '@/utils/notification';
import { api } from '@/packages/api/src';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { DocumentIcon } from '@heroicons/vue/24/solid';
import {
ArrowDownOnSquareIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/outline';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { ImportReport, ImportType } from '@/packages/api/src';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import { initializeStores } from '@/utils/init';
import { CardTitle } from '@/packages/ui/src';
import Card from '@/Components/Common/Card.vue';
const importTypeOptions = ref<ImportType[]>([]);
const { addNotification } = useNotificationsStore();
const loading = ref(false);
onMounted(async () => {
const organizationId = getCurrentOrganizationId();
if (organizationId) {
importTypeOptions.value = (
await api.getImporters({
params: {
organization: organizationId,
},
})
).data;
}
});
const reportResult = ref<ImportReport | null>();
const files = ref<FileList | null>(null);
async function importData() {
if (importType.value === null) {
addNotification('error', 'Please select the import type');
return;
}
if (files.value?.length !== 1) {
addNotification(
'error',
'Please select the CSV or ZIP file that you want to import'
);
return;
}
const rawBase64String = await toBase64(files.value[0]);
const base64String = rawBase64String
.split(';')[1]
.replace('base64,', '') as string;
const organizationId = getCurrentOrganizationId();
if (organizationId !== null) {
const { handleApiRequestNotifications } = useNotificationsStore();
loading.value = true;
try {
reportResult.value = await handleApiRequestNotifications(() => {
if (importType.value) {
return api.importData(
{
type: importType.value.key,
data: base64String,
},
{
params: {
organization: organizationId,
},
}
);
}
return new Promise((resolve, reject) => {
reject('Import type is null');
});
});
initializeStores();
if (reportResult.value) {
showResultModal.value = true;
}
} finally {
loading.value = false;
}
}
}
const importFile = ref<HTMLInputElement | null>();
function toBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
const decoder = new TextDecoder();
const str = decoder.decode(reader.result);
return reject(str);
} else if (reader.result) {
resolve(reader.result);
}
};
reader.onerror = reject;
});
}
function updateFiles() {
files.value = importFile.value?.files ?? null;
}
const currentImporterDescription = computed(() => {
if (importType.value === null) {
return '';
}
return importType.value.description;
});
const filenames = computed(() => {
return files.value?.item(0)?.name ?? 'Import File selected';
});
const importType = ref<ImportType | null>(null);
const showResultModal = ref(false);
</script>
<template>
<DialogModal
closeable
:show="showResultModal"
@close="showResultModal = false">
<template #title>Import Result</template>
<template #content>
<div class="pb-6">
The import was successful! Here is an overview of the imported
data:
</div>
<div
class="py-2.5 px-3 border-t border-t-card-background-separator">
<span class="text-text-primary font-semibold">Clients created:</span>
{{ reportResult?.report.clients.created }}
</div>
<div
class="py-2.5 px-3 border-t border-t-card-background-separator">
<span class="text-text-primary font-semibold">Projects created:</span>
{{ reportResult?.report.projects.created }}
</div>
<div
class="py-2.5 px-3 border-t border-t-card-background-separator">
<span class="text-text-primary font-semibold">Tasks created:</span>
{{ reportResult?.report.tasks.created }}
</div>
<div
class="py-2.5 px-3 border-t border-t-card-background-separator">
<span class="text-text-primary font-semibold"
>Time entries created:</span
>
{{ reportResult?.report.time_entries.created }}
</div>
</template>
<template #footer>
<SecondaryButton @click="showResultModal = false">
Close
</SecondaryButton>
</template>
</DialogModal>
<div>
<CardTitle
title="Import Data"
:icon="ArrowDownOnSquareIcon"></CardTitle>
<Card class="mb-3">
<div class="py-2 px-3 sm:px-4 text-sm flex items-center space-x-3">
<InformationCircleIcon
class="h-5 min-w-0 w-5 text-bg-tertiary" />
<p class="flex-1">
Import existing data from Toggl, Clockify or a different
solidtime instance. Please select the type of data you want
to import and follow the instructions.
</p>
</div>
</Card>
<Card>
<div
class="px-4 py-5 sm:px-5">
<div>
<InputLabel for="importType" value="Import Type" />
<select
id="importType"
v-model="importType"
name="importType"
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option :value="null" selected disabled>
Select an import type to get instructions...
</option>
<option
v-for="importTypeOption in importTypeOptions"
:key="importTypeOption.key"
:value="importTypeOption">
{{ importTypeOption.name }}
</option>
</select>
<div
v-if="currentImporterDescription"
class="py-3 text-text-primary">
<div class="font-semibold text-text-secondary py-1">
Instructions:
</div>
<div
class="max-w-2xl"
v-html="currentImporterDescription"></div>
</div>
</div>
<div
class="mt-2 flex justify-center rounded-lg border border-dashed border-border-primary px-6 py-10">
<div class="text-center">
<DocumentIcon
class="mx-auto h-8 w-8 text-text-secondary"
aria-hidden="true" />
<div class="mt-4 flex text-sm leading-6 text-text-secondary">
<label
for="file-upload"
class="relative cursor-pointer rounded-md font-semibold text-text-primary focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 focus-within:ring-offset-gray-900 hover:text-indigo-500">
<span v-if="files">{{ filenames }}</span>
<span v-else
>Upload a Toggl/Clockify Export</span
>
<input
id="file-upload"
ref="importFile"
name="file-upload"
type="file"
class="sr-only"
@change="updateFiles" />
</label>
</div>
<p class="text-xs leading-5 text-text-secondary">
CSV and ZIP are supported
</p>
</div>
</div>
</div>
<div
class="flex items-center justify-end px-4 py-3 bg-card-background border-t border-card-background-separator text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<PrimaryButton :loading @click="importData"
>Import Data
</PrimaryButton>
</div>
</Card>
</div>
</template>