Skip to content

Commit

Permalink
add import types and description from the server, add report data modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Gregor Vostrak authored and Onatcer committed Apr 16, 2024
1 parent 00da81c commit 165f870
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 30 deletions.
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/V1/ImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function import(Organization $organization, ImportRequest $request, Impor
* tasks: array{
* created: int,
* },
* time-entries: array{
* time_entries: array{
* created: int,
* },
* tags: array{
Expand Down
4 changes: 2 additions & 2 deletions app/Service/Import/Importers/ReportDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function __construct(int $clientsCreated, int $projectsCreated, int $task
* tasks: array{
* created: int,
* },
* time-entries: array{
* time_entries: array{
* created: int,
* },
* tags: array{
Expand All @@ -62,7 +62,7 @@ public function toArray(): array
'tasks' => [
'created' => $this->tasksCreated,
],
'time-entries' => [
'time_entries' => [
'created' => $this->timeEntriesCreated,
],
'tags' => [
Expand Down
42 changes: 41 additions & 1 deletion openapi.json.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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: [
{
Expand Down
127 changes: 103 additions & 24 deletions resources/js/Pages/Teams/Partials/ImportData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,', ''),
},
{
Expand All @@ -55,6 +62,7 @@ async function importData() {
},
}
);
showResultModal.value = true;
}
}
Expand All @@ -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>
Expand All @@ -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
Expand All @@ -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>
Expand Down
4 changes: 3 additions & 1 deletion resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions resources/js/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'>;
2 changes: 1 addition & 1 deletion tests/Unit/Endpoint/Api/V1/ImportEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public function test_import_calls_import_service_if_user_has_permission(): void
'tasks' => [
'created' => 3,
],
'time-entries' => [
'time_entries' => [
'created' => 4,
],
'tags' => [
Expand Down

0 comments on commit 165f870

Please sign in to comment.