mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
add import types and description from the server, add report data modal
This commit is contained in:
@@ -84,7 +84,7 @@ class ImportController extends Controller
|
||||
* tasks: array{
|
||||
* created: int,
|
||||
* },
|
||||
* time-entries: array{
|
||||
* time_entries: array{
|
||||
* created: int,
|
||||
* },
|
||||
* tags: array{
|
||||
|
||||
@@ -39,7 +39,7 @@ class ReportDto
|
||||
* tasks: array{
|
||||
* created: int,
|
||||
* },
|
||||
* time-entries: array{
|
||||
* time_entries: array{
|
||||
* created: int,
|
||||
* },
|
||||
* tags: array{
|
||||
@@ -62,7 +62,7 @@ class ReportDto
|
||||
'tasks' => [
|
||||
'created' => $this->tasksCreated,
|
||||
],
|
||||
'time-entries' => [
|
||||
'time_entries' => [
|
||||
'created' => $this->timeEntriesCreated,
|
||||
],
|
||||
'tags' => [
|
||||
|
||||
@@ -433,7 +433,7 @@ const endpoints = makeApi([
|
||||
tasks: z
|
||||
.object({ created: z.number().int() })
|
||||
.passthrough(),
|
||||
'time-entries': z
|
||||
time_entries: z
|
||||
.object({ created: z.number().int() })
|
||||
.passthrough(),
|
||||
tags: z
|
||||
@@ -476,6 +476,44 @@ const endpoints = makeApi([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/importers',
|
||||
alias: 'getImporters',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string().uuid(),
|
||||
},
|
||||
],
|
||||
response: z
|
||||
.object({
|
||||
data: z.array(
|
||||
z
|
||||
.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
),
|
||||
})
|
||||
.passthrough(),
|
||||
errors: [
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
description: `Not found`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/invitations',
|
||||
@@ -1545,6 +1583,8 @@ const endpoints = makeApi([
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/time-entries',
|
||||
alias: 'getTimeEntries',
|
||||
description: `If you only need time entries for a specific user, you can filter by `user_id`.
|
||||
Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.`,
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
|
||||
@@ -2,51 +2,58 @@
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import type { Organization } from '@/types/models';
|
||||
import { ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { api } from '../../../../../openapi.json.client';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import { DocumentIcon } from '@heroicons/vue/24/solid';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { ImportReport, ImportType } from '@/utils/api';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
|
||||
defineProps<{
|
||||
team: Organization;
|
||||
}>();
|
||||
|
||||
type ImportType =
|
||||
| 'toggl_time_entries'
|
||||
| 'toggl_data_importer'
|
||||
| 'clockify_time_entries'
|
||||
| 'clockify_projects';
|
||||
|
||||
const importTypeOptions: { value: ImportType; label: string }[] = [
|
||||
{ value: 'toggl_time_entries', label: 'Toggl Time Entries' },
|
||||
{ value: 'toggl_data_importer', label: 'Toggl Data Importer' },
|
||||
{ value: 'clockify_time_entries', label: 'Clockify Time Entries' },
|
||||
{ value: 'clockify_projects', label: 'Clockify Projects' },
|
||||
];
|
||||
const importTypeOptions = ref<ImportType[]>([]);
|
||||
|
||||
const { addNotification } = useNotificationsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId) {
|
||||
importTypeOptions.value = (
|
||||
await api.getImporters({
|
||||
params: {
|
||||
organization: organizationId,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
});
|
||||
|
||||
const reportResult = ref<ImportReport>();
|
||||
const files = ref<FileList | null>(null);
|
||||
|
||||
async function importData() {
|
||||
const files = importFile.value?.files ?? [];
|
||||
if (importType.value === null) {
|
||||
addNotification('error', 'Please select the import type');
|
||||
return;
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
if (files.value?.length !== 1) {
|
||||
addNotification(
|
||||
'error',
|
||||
'Please select the CSV or ZIP file that you want to import'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const base64String = await toBase64(files[0]);
|
||||
const base64String = await toBase64(files.value[0]);
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId !== null) {
|
||||
await api.importData(
|
||||
reportResult.value = await api.importData(
|
||||
{
|
||||
type: importType.value,
|
||||
type: importType.value.key,
|
||||
data: base64String.replace('data:text/csv;base64,', ''),
|
||||
},
|
||||
{
|
||||
@@ -55,6 +62,7 @@ async function importData() {
|
||||
},
|
||||
}
|
||||
);
|
||||
showResultModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +85,68 @@ function toBase64(file: File): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
<FormSection @submitted="importData">
|
||||
<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-white 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-white 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-white 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-white font-semibold"
|
||||
>Time entries created:</span
|
||||
>
|
||||
{{ reportResult?.report.time_entries.created }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showResultModal = false">
|
||||
Close
|
||||
</SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<FormSection>
|
||||
<template #title> Import Data</template>
|
||||
|
||||
<template #description>
|
||||
@@ -98,14 +163,24 @@ const importType = ref<ImportType | null>(null);
|
||||
id="currency"
|
||||
v-model="importType"
|
||||
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option value="" disabled>Select a currency</option>
|
||||
<option :value="null" selected disabled>
|
||||
Select an import type to get instructions...
|
||||
</option>
|
||||
<option
|
||||
v-for="importTypeOption in importTypeOptions"
|
||||
:key="importTypeOption.value"
|
||||
:value="importTypeOption.value">
|
||||
{{ importTypeOption.label }}
|
||||
:key="importTypeOption.key"
|
||||
:value="importTypeOption">
|
||||
{{ importTypeOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="py-3 text-white"
|
||||
v-if="currentImporterDescription">
|
||||
<div class="font-semibold text-muted py-1">
|
||||
Instructions:
|
||||
</div>
|
||||
{{ currentImporterDescription }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -119,11 +194,15 @@ const importType = ref<ImportType | null>(null);
|
||||
<label
|
||||
for="file-upload"
|
||||
class="relative cursor-pointer rounded-md bg-gray-900 font-semibold text-white 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>Upload a Toggl/Clockify Export</span>
|
||||
<span v-if="files">{{ filenames }}</span>
|
||||
<span v-else
|
||||
>Upload a Toggl/Clockify Export</span
|
||||
>
|
||||
<input
|
||||
ref="importFile"
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
v-on:change="updateFiles"
|
||||
type="file"
|
||||
class="sr-only" />
|
||||
</label>
|
||||
|
||||
@@ -61,7 +61,9 @@ const updateTeamName = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/billing">
|
||||
<PrimaryButton type="button" v-if="isBillingActivated()">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
v-if="isBillingActivated()">
|
||||
<CreditCardIcon class="w-5 h-5 me-2" />
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -68,3 +68,9 @@ export type MemberIndexResponse = ZodiosResponseByAlias<
|
||||
export type Member = MemberIndexResponse['data'][0];
|
||||
|
||||
export type CreateTagBody = ZodiosBodyByAlias<SolidTimeApi, 'createTag'>;
|
||||
|
||||
export type ImportType = ZodiosResponseByAlias<
|
||||
SolidTimeApi,
|
||||
'getImporters'
|
||||
>['data'][0];
|
||||
export type ImportReport = ZodiosResponseByAlias<SolidTimeApi, 'importData'>;
|
||||
|
||||
@@ -145,7 +145,7 @@ class ImportEndpointTest extends ApiEndpointTestAbstract
|
||||
'tasks' => [
|
||||
'created' => 3,
|
||||
],
|
||||
'time-entries' => [
|
||||
'time_entries' => [
|
||||
'created' => 4,
|
||||
],
|
||||
'tags' => [
|
||||
|
||||
Reference in New Issue
Block a user