diff --git a/laravel_app/app/Http/Controllers/DownloadController.php b/laravel_app/app/Http/Controllers/DownloadController.php index ed72448..774bc5e 100644 --- a/laravel_app/app/Http/Controllers/DownloadController.php +++ b/laravel_app/app/Http/Controllers/DownloadController.php @@ -7,7 +7,7 @@ class DownloadController extends Controller { public function show() { - return view('download.show'); + return view('download.show', ['project' => $project]); } // } diff --git a/laravel_app/app/Http/Controllers/ProjectController.php b/laravel_app/app/Http/Controllers/ProjectController.php index ad96032..04c77f7 100644 --- a/laravel_app/app/Http/Controllers/ProjectController.php +++ b/laravel_app/app/Http/Controllers/ProjectController.php @@ -16,5 +16,5 @@ public function show(Project $project) { return view('projects.show', compact('project')); } - // + } diff --git a/laravel_app/app/Http/Controllers/ProjectReportController.php b/laravel_app/app/Http/Controllers/ProjectReportController.php new file mode 100644 index 0000000..a0a98ca --- /dev/null +++ b/laravel_app/app/Http/Controllers/ProjectReportController.php @@ -0,0 +1,14 @@ +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(); + } + + // + } +} diff --git a/laravel_app/app/Livewire/Download/DownloadGrid.php b/laravel_app/app/Livewire/Download/DownloadGrid.php index e48695d..c625446 100644 --- a/laravel_app/app/Livewire/Download/DownloadGrid.php +++ b/laravel_app/app/Livewire/Download/DownloadGrid.php @@ -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]); } } diff --git a/laravel_app/app/Livewire/Project/ReportRow.php b/laravel_app/app/Livewire/Project/ReportRow.php new file mode 100644 index 0000000..a4ae934 --- /dev/null +++ b/laravel_app/app/Livewire/Project/ReportRow.php @@ -0,0 +1,24 @@ +report->project->download_path . '/' . $this->report->path; + + if (!Storage::exists($filePath)) { + abort(404); + } + + return Storage::download($filePath); + } + +} diff --git a/laravel_app/app/Livewire/Projects/ReportManager.php b/laravel_app/app/Livewire/Projects/ReportManager.php index 94c35a6..2bc96bf 100644 --- a/laravel_app/app/Livewire/Projects/ReportManager.php +++ b/laravel_app/app/Livewire/Projects/ReportManager.php @@ -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 'Invalid week or year'; + } + $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'); + } } diff --git a/laravel_app/app/Models/Project.php b/laravel_app/app/Models/Project.php index 7346bf5..e7e9b3d 100644 --- a/laravel_app/app/Models/Project.php +++ b/laravel_app/app/Models/Project.php @@ -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,11 +101,16 @@ protected static function boot() }); } + public function reports() + { + return $this->hasMany(ProjectReport::class); + } + public function getAttachmentPathAttribute() { return storage_path(sprintf('%s/attachments', $this->download_path)); - return '/storage/' . $this->download_path . '/attachments'; + return '/storage/'.$this->download_path.'/attachments'; } public function boundingBoxes() @@ -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; + } + + } diff --git a/laravel_app/app/Models/ProjectReport.php b/laravel_app/app/Models/ProjectReport.php new file mode 100644 index 0000000..93a3917 --- /dev/null +++ b/laravel_app/app/Models/ProjectReport.php @@ -0,0 +1,68 @@ +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; + + } +} diff --git a/laravel_app/app/Models/Report.php b/laravel_app/app/Models/Report.php deleted file mode 100644 index ce3631e..0000000 --- a/laravel_app/app/Models/Report.php +++ /dev/null @@ -1,11 +0,0 @@ -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; + } +} diff --git a/laravel_app/config/app.php b/laravel_app/config/app.php index 597cf42..548588d 100644 --- a/laravel_app/config/app.php +++ b/laravel_app/config/app.php @@ -188,4 +188,9 @@ // 'Example' => App\Facades\Example::class, ])->toArray(), + /* + * + */ + 'password' => env('LAPTOP_PASSWORD', 'secret'), + ]; diff --git a/laravel_app/database/migrations/2023_10_24_123510_create_reports_table.php b/laravel_app/database/migrations/2023_10_24_123510_create_project_reports_table.php similarity index 67% rename from laravel_app/database/migrations/2023_10_24_123510_create_reports_table.php rename to laravel_app/database/migrations/2023_10_24_123510_create_project_reports_table.php index fb8b205..8085fe8 100644 --- a/laravel_app/database/migrations/2023_10_24_123510_create_reports_table.php +++ b/laravel_app/database/migrations/2023_10_24_123510_create_project_reports_table.php @@ -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(); }); } diff --git a/laravel_app/database/migrations/2023_12_19_131149_create_jobs_table.php b/laravel_app/database/migrations/2023_12_19_131149_create_jobs_table.php new file mode 100644 index 0000000..6098d9b --- /dev/null +++ b/laravel_app/database/migrations/2023_12_19_131149_create_jobs_table.php @@ -0,0 +1,32 @@ +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'); + } +}; diff --git a/laravel_app/database/migrations/2023_12_19_132243_create_failed_jobs_table.php b/laravel_app/database/migrations/2023_12_19_132243_create_failed_jobs_table.php new file mode 100644 index 0000000..249da81 --- /dev/null +++ b/laravel_app/database/migrations/2023_12_19_132243_create_failed_jobs_table.php @@ -0,0 +1,32 @@ +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'); + } +}; diff --git a/laravel_app/package-lock.json b/laravel_app/package-lock.json index 86f5059..b9a5dbb 100644 --- a/laravel_app/package-lock.json +++ b/laravel_app/package-lock.json @@ -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" } diff --git a/laravel_app/package.json b/laravel_app/package.json index 69666fd..4c11eae 100644 --- a/laravel_app/package.json +++ b/laravel_app/package.json @@ -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" } } diff --git a/laravel_app/resources/css/app.css b/laravel_app/resources/css/app.css index 0de2120..34ca985 100644 --- a/laravel_app/resources/css/app.css +++ b/laravel_app/resources/css/app.css @@ -1,7 +1,9 @@ +@import url('https://rsms.me/inter/inter.css'); @tailwind base; @tailwind components; @tailwind utilities; + [x-cloak] { display: none; } diff --git a/laravel_app/resources/js/alpine.js b/laravel_app/resources/js/alpine.js new file mode 100644 index 0000000..1574ca1 --- /dev/null +++ b/laravel_app/resources/js/alpine.js @@ -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(); + diff --git a/laravel_app/resources/js/app.js b/laravel_app/resources/js/app.js index e59d6a0..861ee70 100644 --- a/laravel_app/resources/js/app.js +++ b/laravel_app/resources/js/app.js @@ -1 +1,2 @@ import './bootstrap'; +import('./alpine'); diff --git a/laravel_app/resources/js/history-tabs.js b/laravel_app/resources/js/history-tabs.js new file mode 100644 index 0000000..2f53478 --- /dev/null +++ b/laravel_app/resources/js/history-tabs.js @@ -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 }, + }) +} diff --git a/laravel_app/resources/views/components/badge.blade.php b/laravel_app/resources/views/components/badge.blade.php new file mode 100644 index 0000000..3501a7f --- /dev/null +++ b/laravel_app/resources/views/components/badge.blade.php @@ -0,0 +1,27 @@ +@props([ + 'status' => '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]; + +?> + + + {{ $status }} + diff --git a/laravel_app/resources/views/components/report-manager-properties-modal.blade.php b/laravel_app/resources/views/components/report-manager-properties-modal.blade.php new file mode 100644 index 0000000..e58283d --- /dev/null +++ b/laravel_app/resources/views/components/report-manager-properties-modal.blade.php @@ -0,0 +1,50 @@ +@props([ + 'formData', + /** @var \App\Livewire\Projects\ProjectManager */ + 'reportManager' +]) + + + + + {{ __('Project') }} + + + + {{ __('Report generator for generating reports') }} + + + +
+ + + +
+ + + + +
+
+ {!! $reportManager->dateRange !!} +
+
+ + + + {{ __('Saved.') }} + + + + {{ __('Cancel') }} + + + {{ __('Save') }} + + + +
+
diff --git a/laravel_app/resources/views/download/show.blade.php b/laravel_app/resources/views/download/show.blade.php index 21a22d3..04b8d4f 100644 --- a/laravel_app/resources/views/download/show.blade.php +++ b/laravel_app/resources/views/download/show.blade.php @@ -8,7 +8,7 @@
- @livewire('download.download-grid') +
diff --git a/laravel_app/resources/views/layouts/app.blade.php b/laravel_app/resources/views/layouts/app.blade.php index 6dc9963..6d443a7 100644 --- a/laravel_app/resources/views/layouts/app.blade.php +++ b/laravel_app/resources/views/layouts/app.blade.php @@ -32,13 +32,13 @@ @endif -
+
{{ $slot }}
@stack('modals') - @livewireScripts + @livewireScriptConfig diff --git a/laravel_app/resources/views/livewire/download/download-grid.blade.php b/laravel_app/resources/views/livewire/download/download-grid.blade.php index 8c3ff61..18e4056 100644 --- a/laravel_app/resources/views/livewire/download/download-grid.blade.php +++ b/laravel_app/resources/views/livewire/download/download-grid.blade.php @@ -11,10 +11,10 @@
- @foreach ($directories as $directory) + @foreach ($files as $file)
- {!! Illuminate\Support\Carbon::parse($directory)->format('Y-m-d') !!} + {{ $file }}
@endforeach diff --git a/laravel_app/resources/views/livewire/project/report-row.blade.php b/laravel_app/resources/views/livewire/project/report-row.blade.php new file mode 100644 index 0000000..5326990 --- /dev/null +++ b/laravel_app/resources/views/livewire/project/report-row.blade.php @@ -0,0 +1,21 @@ + + {{ $report->name }} + + @if($report->status == 'pending') + + @else + + @endif + + + lindsay.walton@example.com + + Member + + @if($report->status === 'success') + Download + @endif + + + + diff --git a/laravel_app/resources/views/livewire/projects/download-manager.blade.php b/laravel_app/resources/views/livewire/projects/download-manager.blade.php index d688950..dae8557 100644 --- a/laravel_app/resources/views/livewire/projects/download-manager.blade.php +++ b/laravel_app/resources/views/livewire/projects/download-manager.blade.php @@ -4,7 +4,7 @@
- @livewire('download.download-grid') +
diff --git a/laravel_app/resources/views/livewire/projects/report-manager.blade.php b/laravel_app/resources/views/livewire/projects/report-manager.blade.php index 3a164db..680fb08 100644 --- a/laravel_app/resources/views/livewire/projects/report-manager.blade.php +++ b/laravel_app/resources/views/livewire/projects/report-manager.blade.php @@ -1,9 +1,52 @@ - - Report Details - .. - - +
- - +
+
+
+

Reports

+

+
+
+ + Create Report + +
+
+
+
+
+ + + + + + + + + + + + @foreach($project->reports()->orderBy('created_at', 'desc')->get() as $report) + + @endforeach + +
+ Name + + Status + + Email + + Role + + Edit +
+
+
+
+
+ +
diff --git a/laravel_app/resources/views/livewire/reports/report-manager.blade.php b/laravel_app/resources/views/livewire/reports/report-manager.blade.php index 22e9d71..c70a6a1 100644 --- a/laravel_app/resources/views/livewire/reports/report-manager.blade.php +++ b/laravel_app/resources/views/livewire/reports/report-manager.blade.php @@ -36,7 +36,6 @@ class="col-span-6 sm:col-span-4 mt-4 px-4 py-4 font-mono overflow-hidden break-w -
@@ -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 Report') }} diff --git a/laravel_app/resources/views/projects/show.blade.php b/laravel_app/resources/views/projects/show.blade.php index 7d606ab..4d946fa 100644 --- a/laravel_app/resources/views/projects/show.blade.php +++ b/laravel_app/resources/views/projects/show.blade.php @@ -5,64 +5,37 @@ -
-
+
+
+ + + +
-
-
-
- -
-
- -
-
- -
-
+
+
+ +
+
+ +
-
- @livewire('projects.download-manager', ['project' => $project]); -
-
- @livewire('projects.mailing-manager', ['project' => $project]); - - -
-
- @livewire('projects.report-manager', ['project' => $project]); -
-
+
+ +
diff --git a/laravel_app/routes/web.php b/laravel_app/routes/web.php index d1d743d..2ad39e2 100644 --- a/laravel_app/routes/web.php +++ b/laravel_app/routes/web.php @@ -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'); }); diff --git a/laravel_app/tailwind.config.js b/laravel_app/tailwind.config.js index abfacff..56b4f2d 100644 --- a/laravel_app/tailwind.config.js +++ b/laravel_app/tailwind.config.js @@ -14,7 +14,7 @@ export default { theme: { extend: { fontFamily: { - sans: ['Figtree', ...defaultTheme.fontFamily.sans], + sans: ['Inter var', ...defaultTheme.fontFamily.sans], }, }, }, diff --git a/laravel_app/tests/Unit/Models/ProjectReportTest.php b/laravel_app/tests/Unit/Models/ProjectReportTest.php new file mode 100644 index 0000000..5616434 --- /dev/null +++ b/laravel_app/tests/Unit/Models/ProjectReportTest.php @@ -0,0 +1,102 @@ +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], + ]; + } + + +} + + diff --git a/laravel_app/tests/Unit/Models/ProjectTest.php b/laravel_app/tests/Unit/Models/ProjectTest.php index bb541b4..15536a8 100644 --- a/laravel_app/tests/Unit/Models/ProjectTest.php +++ b/laravel_app/tests/Unit/Models/ProjectTest.php @@ -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 @@ -46,6 +48,100 @@ public function it_can_add_a_mailing_and_attachment() public function when_running_the_seeder_their_are_three_projects(): void { $this->seed(); - $this->assertCount(3,Project::all()); + $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()); } }