This commit is contained in:
Martin Folkerts 2023-12-22 16:55:40 +01:00
parent bd4ac224ad
commit eb1def36a1
35 changed files with 1124 additions and 208 deletions

View file

@ -7,7 +7,7 @@
class DownloadController extends Controller
{
public function show() {
return view('download.show');
return view('download.show', ['project' => $project]);
}
//
}

View file

@ -16,5 +16,5 @@ public function show(Project $project)
{
return view('projects.show', compact('project'));
}
//
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use App\Models\ProjectReport;
use Illuminate\Http\Request;
class ProjectReportController extends Controller
{
public function download(ProjectReport $projectReport)
{
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Jobs;
use App\Models\ProjectReport;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Symfony\Component\Process\Process;
class ProjectReportGeneratorJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 120;
private ProjectReport $projectReport;
/**
* Create a new job instance.
*/
public function __construct(ProjectReport $projectReport)
{
$this->projectReport = $projectReport;
//
}
/**
* Execute the job.
*/
public function handle(): void
{
logger('in handle');
$this->projectReport->weeksAgo();
$projectFolder = base_path('../');
$command = [
sprintf('%sbuild_report.sh', $projectFolder),
sprintf('--filename=%s', $this->projectReport->getFullPathName()),
sprintf('--weeks_ago=%s', $this->projectReport->weeksAgo()),
sprintf('--report_date=%s', $this->projectReport->getReportDate()),
];
// Convert commands array to a single string
$process = new Process($command);
$process->setTimeout(120);
$currentPath = '/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin/Users/mfolkerts/anaconda3/bin:/Library/Apple/usr/bin';
$process->setEnv(['PATH' => $currentPath.':/usr/local/Cellar/pandoc/3.1.8/bin/pandoc']);
// $process->setTimeout(36000); // stel een geschikte timeout in
$process->start();
try {
$myOutput = [];
$process->wait(function ($type, $buffer) use (&$myOutput) {
// $this->stream(to: 'processOutput', content: $buffer);
$myOutput[] = $buffer;
logger($buffer);
});
$this->processOutput = collect($myOutput)->join('\n');
$this->projectReport->setStatusSuccess();
} catch (ProcessFailedException $exception) {
echo $exception->getMessage();
$this->projectReport->setStatusFailed();
}
//
}
}

View file

@ -2,27 +2,21 @@
namespace App\Livewire\Download;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use App\Models\Project;
use Livewire\Component;
class DownloadGrid extends Component
{
public $directories;
private $files = [];
public function mount()
public function mount(Project $project)
{
$this->directories = collect(Storage::directories('chemba/single_images'))
->map(function ($directory) {
$parts = explode('/', $directory);
return Carbon::parse(end($parts));
})->sortByDesc(function ($date, $key) {
return $date;
})->values();
$this->files = $project->getMosaicList();
// dd($this->directories);
}
public function render()
{
return view('livewire.download.download-grid')->with(['directories', $this->directories]);
return view('livewire.download.download-grid')->with(['files' => $this->files]);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Livewire\Project;
use App\Models\ProjectReport;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class ReportRow extends Component
{
public ProjectReport $report;
public function download()
{
$filePath = $this->report->project->download_path . '/' . $this->report->path;
if (!Storage::exists($filePath)) {
abort(404);
}
return Storage::download($filePath);
}
}

View file

@ -2,12 +2,79 @@
namespace App\Livewire\Projects;
use App\Jobs\ProjectReportGeneratorJob;
use App\Models\Project;
use App\Rules\AllMosaicsPresentRule;
use Livewire\Component;
class ReportManager extends Component
{
public $formData = [];
public $project_id;
public $showReportModal = false;
public $listeners = ['refresh' => '$refresh'];
public function openCreateReportModal()
{
$this->resetFormData();
$this->showReportModal = true;
}
public function mount(Project $project)
{
$this->project_id = $project->id;
}
public function render()
{
return view('livewire.projects.report-manager');
$project = Project::find($this->project_id);
return view('livewire.projects.report-manager')->with(compact('project'));
}
private function resetFormData()
{
$this->formData['week'] = now()->weekOfYear;
$this->formData['year'] = now()->year;
}
public function saveProjectReport()
{
sleep(1);
$this->validate([
'formData.week' => ['required', 'integer', 'min:1', 'max:53' ],
'formData.year' => 'required|integer|min:2020|max:'.now()->addYear()->year,
'formData' => [new AllMosaicsPresentRule($this->project_id)],
]);
$newReport = Project::find($this->project_id)
->reports()->create([
'name' => 'Report for week '.$this->formData['week'].' of '.$this->formData['year'],
'week' => $this->formData['week'],
'year' => $this->formData['year'],
'path' => 'reports/week_'.$this->formData['week'].'_'.$this->formData['year'].'.docx',
]);
ProjectReportGeneratorJob::dispatch($newReport);
$this->dispatch('refresh');
$this->showReportModal = false;
}
public function getDateRangeProperty()
{
if (empty($this->formData['week']) || strlen($this->formData['year']) !== 4) {
return '<span class="text-red-500">Invalid week or year</span>';
}
$begin = now()
->setISODate($this->formData['year'], $this->formData['week'])
->startOfWeek();
$end = now()
->setISODate($this->formData['year'], $this->formData['week'])
->endOfWeek();
return $begin->format('Y-m-d').' - '.$end->format('Y-m-d');
}
}

View file

@ -4,18 +4,21 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Project extends Model
{
use HasFactory;
protected $fillable = [
'name',
'mail_template',
'mail_subject',
'mail_frequency',
'mail_day',
'download_path',
];
public static function saveWithFormData(mixed $formData)
@ -65,6 +68,24 @@ private function upsertMailRecipients($formData)
);
}
private function getMosaicPath()
{
return sprintf('%s/%s', $this->download_path, 'weekly_mosaic');
}
public function getMosaicList(): \Illuminate\Support\Collection
{
return collect(Storage::files($this->getMosaicPath()))
->filter(fn($file) => Str::endsWith($file, '.tif'))
->sortByDesc(function ($file) {
$parts = explode('_', str_replace('.tif', '', $file));
$week = $parts[1];
$year = $parts[2];
return $year.sprintf('%02d', $week);
})
->values();
}
protected static function boot()
{
parent::boot(); // TODO: Change the autogenerated stub
@ -80,6 +101,11 @@ protected static function boot()
});
}
public function reports()
{
return $this->hasMany(ProjectReport::class);
}
public function getAttachmentPathAttribute()
{
return storage_path(sprintf('%s/attachments', $this->download_path));
@ -106,4 +132,46 @@ public function downloads()
{
return $this->hasMany(ProjectDownload::class);
}
public function allMosaicsPresent(Carbon $endDate)
{
// end date is in the future
if ($endDate->isFuture()) {
throw new \Exception('End date is in the future');
}
$mosaicsNotPresentInFilesystem = $this->getMosiacFilenameListByEndDate($endDate)
->filter(function ($filename) {
return !$this->getMosaicList()->contains(function ($mosaicFilename) use ($filename) {
return Str::endsWith( $mosaicFilename, substr($filename, -16));
});
});
if ($mosaicsNotPresentInFilesystem->count() === 0) {
return true;
}
$message = sprintf(
'Missing mosaics: %s',
$mosaicsNotPresentInFilesystem->implode(', ')
);
throw new \Exception($message);
}
public function getMosiacFilenameListByEndDate(Carbon $endDate): \Illuminate\Support\Collection
{
$result = collect([]);
for ($i = 0; $i < 4; $i++) {
$week = $endDate->weekOfYear;
$year = $endDate->year;
if ($week === 53) {
$year--;
}
$result->add(sprintf('week_%02d_%04d.tif', $week, $year));
$endDate = $endDate->subWeek();
}
return $result;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
class ProjectReport extends Model
{
protected $fillable = ['name', 'path', 'week', 'year', 'status'];
public function setStatusSuccess()
{
$this->status = 'success';
$this->save();
}
public function setStatusFailed()
{
$this->status = 'failed';
$this->save();
}
public function project()
{
return $this->belongsTo(Project::class);
}
public function weeksAgo()
{
return now()->diffInWeeks(now()->setISODate($this->year, $this->week));
}
public function getFileName()
{
return 'week_'.$this->week.'_'.$this->year;
}
public function getFullPathName()
{
return storage_path('app/'.$this->project->download_path.'/'.$this->path);
}
public function getReportDate()
{
return self::getReportDateForYearAndWeek(
$this->project,
$this->year,
$this->week
)->toDateString();
}
public static function getReportDateForYearAndWeek(Project $project, $year, $week)
{
$date = Carbon::now()->setISODate($year, $week);
$dayOfWeek = Carbon::parse($project->mail_day)->subDay(2)->dayOfWeek;
if ($dayOfWeek == 6) {
$reportDay = $date->startOfWeek()->subDay();
} else {
$reportDay = $date->startOfWeek()->addDays($dayOfWeek);
}
return $reportDay;
}
}

View file

@ -1,11 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Report extends Model
{
protected $fillable = ['name', 'path'];
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use App\Models\Project;
use App\Models\ProjectReport;
class AllMosaicsPresentRule implements Rule
{
protected $projectId;
protected $errorMessage = '';
public function __construct($projectId)
{
$this->projectId = $projectId;
}
public function passes($attribute, $value)
{
try {
$project = Project::find($this->projectId);
if (!$project) {
$this->errorMessage = 'Project not found.';
return false;
}
return $project->allMosaicsPresent(
ProjectReport::getReportDateForYearAndWeek($project, $value['year'], $value['week'])
);
} catch (\Exception $e) {
$this->errorMessage = $e->getMessage();
return false;
}
}
public function message()
{
return $this->errorMessage;
}
}

View file

@ -188,4 +188,9 @@
// 'Example' => App\Facades\Example::class,
])->toArray(),
/*
*
*/
'password' => env('LAPTOP_PASSWORD', 'secret'),
];

View file

@ -11,10 +11,14 @@
*/
public function up(): void
{
Schema::create('reports', function (Blueprint $table) {
Schema::create('project_reports', function (Blueprint $table) {
$table->id();
$table->string('path');
$table->string('name');
$table->integer('week');
$table->integer('year');
$table->string('status')->default('pending');
$table->unsignedBigInteger('project_id');
$table->timestamps();
});
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('failed_jobs');
}
};

View file

@ -1,15 +1,22 @@
{
"name": "Laravel_app",
"name": "laravel_app",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@alpinejs/collapse": "^3.13.3",
"@alpinejs/focus": "^3.13.3",
"@alpinejs/intersect": "^3.13.3",
"@alpinejs/ui": "^3.13.3-beta.4",
"@ryangjchandler/alpine-clipboard": "^2.3.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"alpinejs": "^3.13.3",
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.7",
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.8.0",
@ -22,7 +29,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -30,6 +36,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@alpinejs/collapse": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.13.3.tgz",
"integrity": "sha512-iGO6IzqBwVNxAaoS4XCCJIIg9U/mH7v4vk6wlzxXKFWMXt2yynw93TkdELyjwxQqeNTEGimZRR+2wNf9yhJn0A=="
},
"node_modules/@alpinejs/focus": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.13.3.tgz",
"integrity": "sha512-fTRX/9wOfysyZ1PJ4gHeUnmiNTIgqBDIqKxeP5iMvj1UHD3TFLDXllvoIKH3ezqcsyQZqxd/q1MFM7dlIhkmeg==",
"dependencies": {
"focus-trap": "^6.9.4",
"tabbable": "^5.3.3"
}
},
"node_modules/@alpinejs/intersect": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/@alpinejs/intersect/-/intersect-3.13.3.tgz",
"integrity": "sha512-4iwomnp4VXP1OmFfyol0sEgWdMFmX1WBlc8/gxRt6tO8PeeJjnZMdGjJeY6gNeRWS4g6I9aTUycdsyl6OQNv7w=="
},
"node_modules/@alpinejs/ui": {
"version": "3.13.3-beta.4",
"resolved": "https://registry.npmjs.org/@alpinejs/ui/-/ui-3.13.3-beta.4.tgz",
"integrity": "sha512-Dc6j40tELUqSAIg93Dgi+Carkw8MB+YXm0sILD41vkxw0ByHf5pICCBvhxkcVRg2I/2/6YM/W7i1ZUORNEqrgw=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@ -386,7 +416,6 @@
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -400,7 +429,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@ -409,7 +437,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@ -417,14 +444,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -434,7 +459,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@ -447,7 +471,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": {
"node": ">= 8"
}
@ -456,7 +479,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@ -465,10 +487,23 @@
"node": ">= 8"
}
},
"node_modules/@ryangjchandler/alpine-clipboard": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.3.0.tgz",
"integrity": "sha512-r1YL/LL851vSemjgcca+M6Yz9SNtA9ATul8nJ0n0sAS1W3V1GUWvH0Od2XdQF1r36YJF+/4sUc0eHF/Zexw7dA=="
},
"node_modules/@tailwindcss/aspect-ratio": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz",
"integrity": "sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==",
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
"dev": true,
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
@ -505,17 +540,36 @@
"node": ">=4"
}
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@ -527,8 +581,7 @@
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
@ -587,14 +640,12 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -603,7 +654,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -613,7 +663,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
@ -657,7 +706,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -686,7 +734,6 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
@ -713,7 +760,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@ -737,7 +783,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -745,14 +790,12 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
@ -772,14 +815,12 @@
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.549",
@ -837,7 +878,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@ -853,7 +893,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@ -865,7 +904,6 @@
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@ -874,7 +912,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -882,6 +919,14 @@
"node": ">=8"
}
},
"node_modules/focus-trap": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz",
"integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==",
"dependencies": {
"tabbable": "^5.3.3"
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
@ -932,14 +977,12 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -953,7 +996,6 @@
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -973,7 +1015,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@ -985,7 +1026,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
@ -994,7 +1034,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -1003,14 +1042,12 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@ -1022,7 +1059,6 @@
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
@ -1034,7 +1070,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1043,7 +1078,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@ -1055,7 +1089,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
@ -1064,7 +1097,6 @@
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz",
"integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
@ -1089,7 +1121,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"engines": {
"node": ">=10"
}
@ -1097,8 +1128,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
@ -1122,7 +1152,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": {
"node": ">= 8"
}
@ -1131,7 +1160,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
@ -1174,7 +1202,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -1186,7 +1213,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
@ -1197,7 +1223,6 @@
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"funding": [
{
"type": "github",
@ -1221,7 +1246,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1239,7 +1263,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1248,7 +1271,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -1257,7 +1279,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@ -1266,7 +1287,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1274,20 +1294,17 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@ -1299,7 +1316,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1308,7 +1324,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -1317,7 +1332,6 @@
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -1345,7 +1359,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
@ -1362,7 +1375,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
@ -1381,7 +1393,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
"integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
"dev": true,
"dependencies": {
"lilconfig": "^2.0.5",
"yaml": "^2.1.1"
@ -1410,7 +1421,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
"dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.11"
},
@ -1429,7 +1439,6 @@
"version": "6.0.13",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -1441,8 +1450,7 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
@ -1454,7 +1462,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@ -1474,7 +1481,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"dependencies": {
"pify": "^2.3.0"
}
@ -1483,7 +1489,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@ -1495,7 +1500,6 @@
"version": "1.22.6",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
"integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@ -1512,7 +1516,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@ -1538,7 +1541,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@ -1561,7 +1563,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1570,7 +1571,6 @@
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
"integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@ -1592,7 +1592,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -1600,11 +1599,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tabbable": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
},
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -1641,7 +1644,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0"
}
@ -1650,7 +1652,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
@ -1662,7 +1663,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
@ -1673,8 +1673,7 @@
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
@ -1709,8 +1708,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "4.4.11",
@ -1783,14 +1781,12 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yaml": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
"integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
"dev": true,
"engines": {
"node": ">= 14"
}

View file

@ -6,8 +6,8 @@
"build": "vite build"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.7",
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.8.0",
@ -16,6 +16,13 @@
"vite": "^4.0.0"
},
"dependencies": {
"@alpinejs/collapse": "^3.13.3",
"@alpinejs/focus": "^3.13.3",
"@alpinejs/intersect": "^3.13.3",
"@alpinejs/ui": "^3.13.3-beta.4",
"@ryangjchandler/alpine-clipboard": "^2.3.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"alpinejs": "^3.13.3",
"tailwindcss": "^3.3.3"
}
}

View file

@ -1,7 +1,9 @@
@import url('https://rsms.me/inter/inter.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
[x-cloak] {
display: none;
}

View file

@ -0,0 +1,22 @@
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
// import Intersect from "@alpinejs/intersect";
import focus from "@alpinejs/focus";
// import Clipboard from "@ryangjchandler/alpine-clipboard";
// import collapse from "@alpinejs/collapse";
import ui from "@alpinejs/ui";
Alpine.plugin(ui)
Alpine.plugin(focus)
// Alpine.plugin(Clipboard);
// Alpine.plugin(Intersect);
// Alpine.plugin(collapse);
import htabs from './history-tabs';
htabs(Alpine)
window.Alpine = Alpine
document.addEventListener("alpine:init", () => {
console.log("Alpine Initialized")
});
Livewire.start();

View file

@ -1 +1,2 @@
import './bootstrap';
import('./alpine');

View file

@ -0,0 +1,167 @@
export default function (Alpine) {
Alpine.directive('htabs', (el, directive) => {
if (! directive.value) handleRoot(el, Alpine)
else if (directive.value === 'list') handleList(el, Alpine)
else if (directive.value === 'tab') handleTab(el, Alpine)
else if (directive.value === 'panels') handlePanels(el, Alpine)
else if (directive.value === 'panel') handlePanel(el, Alpine)
}).before('bind')
Alpine.magic('htab', el => {
let $data = Alpine.$data(el)
return {
get isSelected() {
return $data.__selectedIndex === $data.__tabs.indexOf($data.__tabEl)
},
get isDisabled() {
return $data.__isDisabled
}
}
})
Alpine.magic('hpanel', el => {
let $data = Alpine.$data(el)
return {
get isSelected() {
return $data.__selectedIndex === $data.__panels.indexOf($data.__panelEl)
}
}
})
}
function handleRoot(el, Alpine) {
Alpine.bind(el, {
'x-modelable': '__selectedIndex',
'x-data'() {
return {
init() {
window.addEventListener('popstate', this.__handlePopState.bind(this));
queueMicrotask(() => {
let defaultIndex = this.__selectedIndex || Number(Alpine.bound(this.$el, 'default-index', 0))
let tabs = this.__activeTabs()
let clamp = (number, min, max) => Math.min(Math.max(number, min), max)
this.__selectedIndex = clamp(defaultIndex, 0, tabs.length -1)
Alpine.effect(() => {
this.__manualActivation = Alpine.bound(this.$el, 'manual', false)
})
this.__syncTabWithUrl();
})
},
__tabs: [],
__panels: [],
__selectedIndex: null,
__tabGroupEl: undefined,
__manualActivation: false,
__addTab(el) { this.__tabs.push(el) },
__addPanel(el) { this.__panels.push(el) },
__selectTab(el) {
if (this.__selectedIndex !== this.__tabs.indexOf(el)) {
this.__selectedIndex = this.__tabs.indexOf(el)
this.__updateUrl();
}
},
__updateUrl() {
const selectedTab = this.__tabs[this.__selectedIndex];
const tabName = selectedTab.getAttribute('data-tab-name');
const url = new URL(window.location);
url.searchParams.set('selected-tab', tabName);
window.history.pushState({ tabIndex: this.__selectedIndex }, '', url);
},
__handlePopState() {
this.__syncTabWithUrl();
},
__syncTabWithUrl() {
const urlParams = new URLSearchParams(window.location.search);
const tabName = urlParams.get('selected-tab');
const tabIndex = this.__tabs.findIndex(tab => tab.getAttribute('data-tab-name') === tabName);
this.__selectedIndex = 0;
if (tabIndex >= 0) {
this.__selectedIndex = tabIndex;
}
},
__activeTabs() {
return this.__tabs.filter(i => !i.__disabled)
},
}
}
})
}
function handleList(el, Alpine) {
Alpine.bind(el, {
'x-init'() { this.$data.__tabGroupEl = this.$el }
})
}
function handleTab(el, Alpine) {
Alpine.bind(el, {
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button' },
'x-data'() { return {
init() {
this.__tabEl = this.$el
this.$data.__addTab(this.$el)
this.__tabEl.__disabled = Alpine.bound(this.$el, 'disabled', false)
this.__isDisabled = this.__tabEl.__disabled
},
__tabEl: undefined,
__isDisabled: false,
}},
'@click'() {
if (this.$el.__disabled) return
this.$data.__selectTab(this.$el)
this.$el.focus()
},
'@keydown.enter.prevent.stop'() { this.__selectTab(this.$el) },
'@keydown.space.prevent.stop'() { this.__selectTab(this.$el) },
'@keydown.home.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
'@keydown.page-up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
'@keydown.end.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
'@keydown.page-down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
'@keydown.down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
'@keydown.right.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
'@keydown.up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
'@keydown.left.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
':tabindex'() { return this.$tab.isSelected ? 0 : -1 },
'@focus'() {
if (this.$data.__manualActivation) {
this.$el.focus()
} else {
if (this.$el.__disabled) return
this.$data.__selectTab(this.$el)
this.$el.focus()
}
},
})
}
function handlePanels(el, Alpine) {
Alpine.bind(el, {
//
})
}
function handlePanel(el, Alpine) {
Alpine.bind(el, {
':tabindex'() { return this.$panel.isSelected ? 0 : -1 },
'x-data'() { return {
init() {
this.__panelEl = this.$el
this.$data.__addPanel(this.$el)
},
__panelEl: undefined,
}},
'x-show'() { return this.$panel.isSelected },
})
}

View file

@ -0,0 +1,27 @@
@props([
'status' => 'success',
])
<?php
// Define a mapping of status to its corresponding colors
$statusToColors = [
'success' => ['bg' => 'bg-green-100', 'text' => 'text-green-700'],
'error' => ['bg' => 'bg-red-100', 'text' => 'text-red-700'],
'warning' => ['bg' => 'bg-yellow-100', 'text' => 'text-yellow-800'],
'info' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700'],
'pending' => ['bg' => 'bg-gray-100', 'text' => 'text-gray-600'],
];
// Default to 'success' if the provided status is not in the defined array
if (!array_key_exists($status, $statusToColors)) {
$status = 'success';
}
// Get the color class for the given status
$colorClasses = $statusToColors[$status];
?>
<span {{ $attributes }} class="inline-flex items-center rounded-md {{ $colorClasses['bg'] }} px-1.5 py-0.5 text-xs font-medium {{ $colorClasses['text'] }}">
{{ $status }}
</span>

View file

@ -0,0 +1,50 @@
@props([
'formData',
/** @var \App\Livewire\Projects\ProjectManager */
'reportManager'
])
<x-modal wire:model.live="showReportModal" {{ $attributes }} >
<x-form-modal submit="saveProjectReport" wire:loading.class="opacity-50">
<x-slot name="title">
{{ __('Project') }}
</x-slot>
<x-slot name="description">
{{ __('Report generator for generating reports') }}
</x-slot>
<x-slot name="form">
<div class="col-span-6 sm:col-span-4">
<x-label for="year" value="{{ __('Year') }}"/>
<x-input id="year" type="text" class="mt-1 block w-full" wire:model.live="formData.year" autofocus/>
<x-input-error for="formData.year" class="mt-2"/>
</div><div class="col-span-6 sm:col-span-4">
<x-label for="week" value="{{ __('Week') }}"/>
<x-input id="week" type="text" class="mt-1 block w-full" wire:model.live="formData.week" autofocus/>
<x-input-error for="formData.week" class="mt-2"/>
<x-input-error for="formData" class="mt-2"/>
</div>
<div>
<span class="whitespace-nowrap">{!! $reportManager->dateRange !!}</span>
</div>
</x-slot>
<x-slot name="actions">
<x-action-message class="mr-3" on="saved">
{{ __('Saved.') }}
</x-action-message>
<x-secondary-button class="mr-3"
type="button"
x-on:click="$wire.showReportModal = false"
>
{{ __('Cancel') }}
</x-secondary-button>
<x-button wire:loading.disabled>
{{ __('Save') }}
</x-button>
</x-slot>
</x-form-modal>
</x-modal>

View file

@ -8,7 +8,7 @@
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<div class="mt-10 sm:mt-0">
@livewire('download.download-grid')
<livewire:download.download-grid :project="$project" />
</div>
<x-section-border />

View file

@ -32,13 +32,13 @@
@endif
<!-- Page Content -->
<main>
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $slot }}
</main>
</div>
@stack('modals')
@livewireScripts
@livewireScriptConfig
</body>
</html>

View file

@ -11,10 +11,10 @@
<!-- API Token List -->
<x-slot name="content">
<div class="space-y-6">
@foreach ($directories as $directory)
@foreach ($files as $file)
<div class="flex items-center justify-between">
<div class="break-all">
{!! Illuminate\Support\Carbon::parse($directory)->format('Y-m-d') !!}
{{ $file }}
</div>
</div>
@endforeach

View file

@ -0,0 +1,21 @@
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">{{ $report->name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
@if($report->status == 'pending')
<x-badge status="pending" wire:poll.1s=""></x-badge>
@else
<x-badge :status="$report->status"></x-badge>
@endif
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
lindsay.walton@example.com
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">Member</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
@if($report->status === 'success')
<x-button wire:click="download">Download</x-button>
@endif
</td>
</tr>

View file

@ -4,7 +4,7 @@
<x-slot name="form">
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<div class="mt-10 sm:mt-0">
@livewire('download.download-grid')
<livewire:download.download-grid :project="$project"/>
</div>
<x-section-border />

View file

@ -1,9 +1,52 @@
<x-tab-section>
<x-slot name="title">Report Details</x-slot>
<x-slot name="description">..</x-slot>
<x-slot name="form">
<div>
</x-slot>
</x-tab-section>
<div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">Reports</h1>
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<x-button wire:click="openCreateReportModal"
class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Create Report
</x-button>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8">
Name
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Email
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Role
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
@foreach($project->reports()->orderBy('created_at', 'desc')->get() as $report)
<livewire:project.report-row :$report :key="$report->id"/>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<x-report-manager-properties-modal :reportManager="$this"/>
</div>

View file

@ -36,7 +36,6 @@ class="col-span-6 sm:col-span-4 mt-4 px-4 py-4 font-mono overflow-hidden break-w
<x-section-border />
<!-- Manage API Tokens -->
<div class="mt-10 sm:mt-0">
<x-action-section>
<x-slot name="title">
@ -71,7 +70,6 @@ class="col-span-6 sm:col-span-4 mt-4 px-4 py-4 font-mono overflow-hidden break-w
<!-- Delete Token Report Modal -->
<x-confirmation-modal wire:model.live="confirmingReportDeletion">
<x-slot name="title">
{{ __('Delete Report') }}

View file

@ -5,64 +5,37 @@
</h2>
</x-slot>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8" x-data="{openTab: 1}">
<div class="mt-10 sm:mt-0">
<div class="flex w-full space-x-6 mb-5 border-b border-secondary max-h-[50px]" selid="tabs">
<div :class="{'border-b-2 border-indigo-500 -mb-px text-indigo-500' : openTab === 1}" selid="tab-question">
<button
style="color:inherit"
x-on:click="openTab = 1"
>
<span>{{ __('Mailing') }}</span>
</button>
</div>
<div class="" :class="{'border-b-2 border-indigo-500 -mb-px text-indigo-500' : openTab === 2}"
selid="tab-settings">
<button
style="color:inherit"
x-on:click="openTab = 2;"
>
<span>{{ __('Downloads') }}</span>
</button>
</div>
<div class="" :class="{'border-b-2 border-indigo-500 -mb-px text-indigo-500' : openTab === 3}"
selid="tab-statistics">
<button
style="color:inherit"
x-on:click="openTab = 3;"
>
<span>{{ __('Reports') }}</span>
</button>
</div>
<div x-data x-htabs class="flex">
<div x-tabs:list class="-mr-px flex items-stretch flex-col">
<button x-tabs:tab type="button"
data-tab-name="downloads"
:class="$tab.isSelected ? 'border-gray-200 bg-white' : 'border-transparent'"
class="inline-flex rounded-l-md border-t border-l border-b px-5 py-2.5"
>{{ __('Downloads') }}</button>
<button x-tabs:tab type="button"
data-tab-name="mailing"
:class="$tab.isSelected ? 'border-gray-200 bg-white' : 'border-transparent'"
class="inline-flex rounded-l-md border-t border-l border-b px-5 py-2.5"
>{{ __('Mailing') }}</button>
<button x-tabs:tab type="button"
data-tab-name="reports"
:class="$tab.isSelected ? 'border-gray-200 bg-white' : 'border-transparent'"
class="inline-flex rounded-l-md border-t border-l border-b px-5 py-2.5"
>{{ __('Reports') }}</button>
</div>
<div x-tabs:panels class="rounded-b-md border border-gray-200 bg-white w-full">
<section x-tabs:panel class="p-8">
<livewire:projects.download-manager :project="$project" />
</section>
<div class="flex flex-col flex-1 pb-5 space-y-4 relative" x-show="openTab === 2"
x-transition:enter="transition duration-200"
x-transition:enter-start="opacity-0 delay-200"
x-transition:enter-end="opacity-100"
>
@livewire('projects.download-manager', ['project' => $project]);
</div>
<div class="flex flex-col flex-1 pb-5 space-y-4 relative" x-show="openTab === 1"
x-transition:enter="transition duration-200"
x-transition:enter-start="opacity-0 delay-200"
x-transition:enter-end="opacity-100"
>
@livewire('projects.mailing-manager', ['project' => $project]);
<section x-tabs:panel class="p-8">
<livewire:projects.mailing-manager :project="$project" />
</section>
</div>
<div class="flex flex-col flex-1 pb-5 space-y-4 relative" x-show="openTab === 3"
x-transition:enter="transition duration-200"
x-transition:enter-start="opacity-0 delay-200"
x-transition:enter-end="opacity-100"
>
@livewire('projects.report-manager', ['project' => $project]);
</div>
</div>
<section x-tabs:panel class="p-8">
<livewire:projects.report-manager :project="$project" />
</section>
</div>
</div>
</x-app-layout>

View file

@ -29,9 +29,9 @@
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/download', [\App\Http\Controllers\DownloadController::class, 'show'])->name('download');
Route::get('/report', [\App\Http\Controllers\ReportController::class, 'index'])->name('report');
Route::get('/projects/{project}', [\App\Http\Controllers\ProjectController::class, 'show'])->name('project.show');
Route::get('/projects/{projectReport}/download', [\App\Http\Controllers\ProjectReportController::class, 'download'])->name('project.report.download');
Route::get('/projects', [\App\Http\Controllers\ProjectController::class, 'index'])->name('project');
});

View file

@ -14,7 +14,7 @@ export default {
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
},
},

View file

@ -0,0 +1,102 @@
<?php
namespace Tests\Unit\Models;
use App\Models\Project;
use App\Models\ProjectReport;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Mockery;
use Tests\TestCase;
class ProjectReportTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
}
/**
* @test
* @dataProvider weeksAgoProvider
*/
public function it_can_calculatore_weeksAgo($year, $week, $expected)
{
Carbon::setTestNow(Carbon::now()->setISODate('2021', '41'));
$projectReport = ProjectReport::make([
'year' => $year,
'week' => $week,
]);
$this->assertEquals($expected, $projectReport->weeksAgo());
}
public static function weeksAgoProvider()
{
return [
'1 week ago' => [2021, 40, 1],
'2 weeks ago' => [2021, 39, 2],
'3 weeks ago' => [2021, 38, 3],
];
}
/**
* @test
*/
public function it_can_get_the_full_path_name()
{
$project = Project::create([
'name' => 'project_name',
'download_path' => 'project_download_path',
]);
$projectReport = $project->reports()->create([
'name' => 'name',
'year' => 2021,
'week' => 41,
'path' => 'path/doc.pdf',
'status' => 'success',
]);
$this->assertEquals(storage_path('app/project_download_path/path/doc.pdf'), $projectReport->getFullPathName());
}
/**
* @test
* @dataProvider reportDateProvider
*/
public function it_can_return_the_reportDate($expected, $mail_day, $week, $year)
{
$project = Project::create([
'name' => 'project_name',
'download_path' => 'project_download_path',
'mail_day' => $mail_day,
]);
$projectReport = $project->reports()->create([
'name' => 'name',
'year' => $year,
'week' => $week,
'path' => 'path/doc.pdf',
'status' => 'success',
]);
$this->assertEquals($expected, $projectReport->getReportDate());
}
public static function reportDateProvider()
{
return [
'monday' => ['2023-12-10' , 'monday', 50, 2023],
'tuesday' => ['2023-12-11' , 'tuesday', 50, 2023],
'wednesday' => ['2023-12-12' , 'wednesday', 50, 2023],
'thursday' => ['2023-12-13' , 'thursday', 50, 2023],
'friday' => ['2023-12-14' , 'friday', 50, 2023],
'saturday' => ['2023-12-15' , 'saturday', 50, 2023],
'sunday' => ['2023-12-16' , 'sunday', 50, 2023],
];
}
}

View file

@ -5,6 +5,8 @@
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Mockery;
use Tests\TestCase;
class ProjectTest extends TestCase
@ -48,4 +50,98 @@ public function when_running_the_seeder_their_are_three_projects(): void
$this->seed();
$this->assertCount(3, Project::all());
}
/** @test */
public function when_running_the_seeder_their_are_three_projects_with_the_correct_names(): void
{
$this->seed();
$this->assertEquals('Chemba', Project::find(1)->name);
$this->assertEquals('Xinavane', Project::find(2)->name);
$this->assertEquals('Kakira', Project::find(3)->name);
}
/** @test */
public function when_running_the_seeder_their_are_three_projects_with_the_correct_download_paths(): void
{
$this->seed();
$this->assertEquals('chemba', Project::find(1)->download_path);
$this->assertEquals('xinavane', Project::find(2)->download_path);
$this->assertEquals('kakira', Project::find(3)->download_path);
}
/** @test */
public function when_not_all_mosaics_are_present_it_should_return_an_exception()
{
$this->seed();
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Missing mosaics: week_52_2022.tif, week_51_2021.tif, week_50_2021.tif, week_49_2021.tif');
$project = Project::find(1);
$lastDate = Carbon::parse('2022-01-01');
//$lastDate->getWeekOfYear();
$project->allMosaicsPresent($lastDate);
}
/** @test */
public function when_all_mosaics_are_present_it_should_return_true()
{
$project = Mockery::mock(Project::class)->makePartial();
$lastDate = Carbon::parse('2021-01-01');
$project->shouldReceive('getMosaicList')->andReturn(
collect([
"chemba/weekly_mosaic/week_53_2020.tif",
"chemba/weekly_mosaic/week_52_2020.tif",
"chemba/weekly_mosaic/week_51_2020.tif",
"chemba/weekly_mosaic/week_50_2020.tif",
]));
$this->assertTrue($project->allMosaicsPresent($lastDate));
}
/** @test */
public function when_not_mosaics_are_present_it_should_throw_an_exception_listing_the_missing_mosiacs()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Missing mosaics: week_53_2020.tif');
$project = Mockery::mock(Project::class)->makePartial();
$lastDate = Carbon::parse('2021-01-01');
$project->shouldReceive('getMosaicList')->andReturn(
collect([
"chemba/weekly_mosaic/week_52_2020.tif",
"chemba/weekly_mosaic/week_51_2020.tif",
"chemba/weekly_mosaic/week_50_2020.tif",
"chemba/weekly_mosaic/week_49_2021.tif",
"chemba/weekly_mosaic/week_48_2021.tif",
]));
$project->allMosaicsPresent($lastDate);
}
/** @test */
public function when_all_mosaics_are_present_is_called_with_future_date_it_should_throw_an_expection()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('End date is in the future');
$this->seed();
$project = Project::find(1);
Carbon::setTestNow(Carbon::parse('2020-01-01'));
$lastDate = Carbon::parse('2021-01-01');
// stub getMosiacList() to return a list containing four filenames;
($project->allMosaicsPresent($lastDate));
}
/** @test */
public function getMosiacFileListByEndDate_should_return_four_filenames()
{
$this->seed();
$project = Project::find(1);
$lastDate = Carbon::parse('2021-01-01');
$list = $project->getMosiacFilenameListByEndDate($lastDate);
$this->assertCount(4, $list);
$this->assertEquals([
"week_53_2020.tif",
"week_52_2020.tif",
"week_51_2020.tif",
"week_50_2020.tif",
], $list->toArray());
}
}