mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
390 lines
16 KiB
PHP
390 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\Export;
|
|
|
|
use App\Models\Client;
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Models\OrganizationInvitation;
|
|
use App\Models\Project;
|
|
use App\Models\ProjectMember;
|
|
use App\Models\Tag;
|
|
use App\Models\Task;
|
|
use App\Models\TimeEntry;
|
|
use Exception;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Http\File;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use League\Csv\CannotInsertRecord;
|
|
use League\Csv\Exception as LeagueCsvException;
|
|
use League\Csv\UnavailableStream;
|
|
use League\Csv\Writer;
|
|
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
|
use ZipArchive;
|
|
|
|
class ExportService
|
|
{
|
|
public const string VERSION = '1.0';
|
|
|
|
/**
|
|
* @throws ExportException
|
|
*/
|
|
public function export(Organization $organization): string
|
|
{
|
|
$exportId = Str::uuid();
|
|
$timeStamp = Carbon::now();
|
|
$temporaryDirectory = TemporaryDirectory::make();
|
|
Log::debug('Start exporting organization', [
|
|
'organization_id' => $organization->getKey(),
|
|
'export_id' => $exportId,
|
|
]);
|
|
|
|
// Organizations
|
|
try {
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('organizations.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'name',
|
|
'billable_rate',
|
|
'currency',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
$writer->insertOne([
|
|
$organization->id,
|
|
$organization->name,
|
|
$organization->billable_rate ?? '',
|
|
$organization->currency,
|
|
$organization->created_at?->toIso8601ZuluString() ?? '',
|
|
$organization->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
|
|
// Organization invitations
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('organization_invitations.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'email',
|
|
'organization_id',
|
|
'role',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
OrganizationInvitation::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $organizationInvitations) use (&$writer): void {
|
|
$organizationInvitations->each(function (OrganizationInvitation $organizationInvitation) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$organizationInvitation->id,
|
|
$organizationInvitation->email,
|
|
$organizationInvitation->organization_id,
|
|
$organizationInvitation->role,
|
|
$organizationInvitation->created_at?->toIso8601ZuluString() ?? '',
|
|
$organizationInvitation->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Time entries
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('time_entries.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'description',
|
|
'start',
|
|
'end',
|
|
'billable_rate',
|
|
'billable',
|
|
'member_id',
|
|
'user_id',
|
|
'organization_id',
|
|
'client_id',
|
|
'project_id',
|
|
'task_id',
|
|
'tags',
|
|
'is_imported',
|
|
'still_active_email_sent_at',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
TimeEntry::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $timeEntries) use (&$writer): void {
|
|
$timeEntries->each(function (TimeEntry $timeEntry) use (&$writer): void {
|
|
$tags = json_encode($timeEntry->tags);
|
|
$writer->insertOne([
|
|
$timeEntry->id,
|
|
$timeEntry->description,
|
|
$timeEntry->start->toIso8601ZuluString(),
|
|
$timeEntry->end?->toIso8601ZuluString() ?? '',
|
|
$timeEntry->billable_rate ?? '',
|
|
$timeEntry->billable ? 'true' : 'false',
|
|
$timeEntry->member_id,
|
|
$timeEntry->user_id,
|
|
$timeEntry->organization_id,
|
|
$timeEntry->client_id ?? '',
|
|
$timeEntry->project_id ?? '',
|
|
$timeEntry->task_id ?? '',
|
|
$tags === false ? '' : $tags,
|
|
$timeEntry->is_imported ? 'true' : 'false',
|
|
$timeEntry->still_active_email_sent_at?->toIso8601ZuluString() ?? '',
|
|
$timeEntry->created_at?->toIso8601ZuluString() ?? '',
|
|
$timeEntry->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Clients
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('clients.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'name',
|
|
'organization_id',
|
|
'archived_at',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
Client::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $clients) use (&$writer): void {
|
|
$clients->each(function (Client $client) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$client->id,
|
|
$client->name,
|
|
$client->organization_id,
|
|
$client->archived_at ?? '',
|
|
$client->created_at?->toIso8601ZuluString() ?? '',
|
|
$client->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Projects
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('projects.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'name',
|
|
'color',
|
|
'billable_rate',
|
|
'is_public',
|
|
'client_id',
|
|
'organization_id',
|
|
'is_billable',
|
|
'archived_at',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
Project::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $projects) use (&$writer): void {
|
|
$projects->each(function (Project $project) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$project->id,
|
|
$project->name,
|
|
$project->color,
|
|
$project->billable_rate ?? '',
|
|
$project->is_public ? 'true' : 'false',
|
|
$project->client_id ?? '',
|
|
$project->organization_id,
|
|
$project->is_billable ? 'true' : 'false',
|
|
$project->archived_at?->toIso8601ZuluString() ?? '',
|
|
$project->created_at?->toIso8601ZuluString() ?? '',
|
|
$project->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Project members
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('project_members.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'billable_rate',
|
|
'project_id',
|
|
'user_id',
|
|
'member_id',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
ProjectMember::query()
|
|
->whereBelongsToOrganization($organization)
|
|
->chunk(1000, function (Collection $projectMembers) use (&$writer): void {
|
|
$projectMembers->each(function (ProjectMember $projectMember) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$projectMember->id,
|
|
$projectMember->billable_rate ?? '',
|
|
$projectMember->project_id,
|
|
$projectMember->user_id,
|
|
$projectMember->member_id,
|
|
$projectMember->created_at?->toIso8601ZuluString() ?? '',
|
|
$projectMember->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Members
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('members.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'user_id',
|
|
'name',
|
|
'email',
|
|
'organization_id',
|
|
'billable_rate',
|
|
'role',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
Member::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->with([
|
|
'user',
|
|
])
|
|
->chunk(1000, function (Collection $members) use (&$writer): void {
|
|
$members->each(function (Member $member) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$member->id,
|
|
$member->user_id,
|
|
$member->user->name,
|
|
$member->user->email,
|
|
$member->organization_id,
|
|
$member->billable_rate ?? '',
|
|
$member->role,
|
|
$member->created_at?->toIso8601ZuluString() ?? '',
|
|
$member->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Tasks
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('tasks.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'name',
|
|
'project_id',
|
|
'organization_id',
|
|
'done_at',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
Task::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $tasks) use (&$writer): void {
|
|
$tasks->each(function (Task $task) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$task->id,
|
|
$task->name,
|
|
$task->project_id,
|
|
$task->organization_id,
|
|
$task->done_at?->toIso8601ZuluString() ?? '',
|
|
$task->created_at?->toIso8601ZuluString() ?? '',
|
|
$task->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Tags
|
|
$writer = Writer::createFromPath($temporaryDirectory->path('tags.csv'), 'w+');
|
|
$writer->setDelimiter(',');
|
|
$writer->setEnclosure('"');
|
|
$writer->setEscape('');
|
|
$writer->insertOne([
|
|
'id',
|
|
'name',
|
|
'organization_id',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
Tag::query()
|
|
->whereBelongsTo($organization, 'organization')
|
|
->chunk(1000, function (Collection $tags) use (&$writer): void {
|
|
$tags->each(function (Tag $tag) use (&$writer): void {
|
|
$writer->insertOne([
|
|
$tag->id,
|
|
$tag->name,
|
|
$tag->organization_id,
|
|
$tag->created_at?->toIso8601ZuluString() ?? '',
|
|
$tag->updated_at?->toIso8601ZuluString() ?? '',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Meta data file
|
|
$metaData = (object) [
|
|
'id' => $exportId,
|
|
'version' => self::VERSION,
|
|
'organizations' => [$organization->getKey()],
|
|
'exported_at' => $timeStamp->toIso8601ZuluString(),
|
|
];
|
|
file_put_contents($temporaryDirectory->path('meta.json'), json_encode($metaData));
|
|
|
|
// Create ZIP file
|
|
$temporaryDirectoryZip = TemporaryDirectory::make();
|
|
$zip = new ZipArchive;
|
|
if ($zip->open($temporaryDirectoryZip->path('export.zip'), ZipArchive::CREATE) !== true) {
|
|
throw new Exception('Cannot create ZIP file');
|
|
}
|
|
$zip->addFile($temporaryDirectory->path('organizations.csv'), 'organizations.csv');
|
|
$zip->addFile($temporaryDirectory->path('organization_invitations.csv'), 'organization_invitations.csv');
|
|
$zip->addFile($temporaryDirectory->path('time_entries.csv'), 'time_entries.csv');
|
|
$zip->addFile($temporaryDirectory->path('clients.csv'), 'clients.csv');
|
|
$zip->addFile($temporaryDirectory->path('projects.csv'), 'projects.csv');
|
|
$zip->addFile($temporaryDirectory->path('project_members.csv'), 'project_members.csv');
|
|
$zip->addFile($temporaryDirectory->path('members.csv'), 'members.csv');
|
|
$zip->addFile($temporaryDirectory->path('tasks.csv'), 'tasks.csv');
|
|
$zip->addFile($temporaryDirectory->path('tags.csv'), 'tags.csv');
|
|
$zip->addFile($temporaryDirectory->path('meta.json'), 'meta.json');
|
|
$zip->close();
|
|
|
|
// Upload ZIP file to private storage
|
|
$filename = 'export_'.$organization->getKey().'_'.$timeStamp->format('Y-m-d_H-i-s').'_'.$exportId.'.zip';
|
|
Storage::disk(config('filesystems.private'))->putFileAs(
|
|
'exports',
|
|
new File($temporaryDirectoryZip->path('export.zip')),
|
|
$filename
|
|
);
|
|
|
|
// Delete temp files
|
|
$temporaryDirectoryZip->delete();
|
|
$temporaryDirectory->delete();
|
|
|
|
Log::debug('Finished exporting organization', [
|
|
'organization_id' => $organization->getKey(),
|
|
'export_id' => $exportId,
|
|
]);
|
|
|
|
return 'exports/'.$filename;
|
|
} catch (UnavailableStream|CannotInsertRecord|Exception|LeagueCsvException $exception) {
|
|
report($exception);
|
|
|
|
throw new ExportException;
|
|
}
|
|
}
|
|
}
|