Added new tab components for projects management

This commit is contained in:
guillaume91 2024-05-23 15:37:49 +02:00
parent fc6edb2444
commit 279bd3ec1a
13 changed files with 615 additions and 46 deletions

View file

@ -12,9 +12,13 @@ public function index()
return view('projects.index');
}
public function show(Project $project,?string $currentTab = null)
public function show(string $projectName,?string $currentTab = null)
{
return $currentTab ? view('projects.show', compact(['project', 'currentTab'])) : redirect(route('project.show', [$project->id, 'download']));
if($project = Project::firstWhere('name',$projectName)) {
$availableTabs = ['downloads', 'mosaics', 'reports', 'mailings', 'settings'];
return in_array($currentTab, $availableTabs) ? view('projects.show', compact(['project', 'currentTab'])) : redirect(route('project.show', [$projectName, 'downloads']));
}
return abort(404);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Livewire\Projects;
use Livewire\Component;
class Menu extends Component
{
public string $activeTab = "downloads";
public string $projectName = '';
public function mount()
{
}
public function render()
{
return view('livewire.projects.menu');
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace App\Livewire\Projects\Tabs;
use App\Models\Project;
use App\Models\ProjectMailing;
use Livewire\Component;
use Livewire\WithPagination;
class Mailings extends Component
{
use WithPagination;
public $project;
public $mailingDetailsModal = false;
public $formData = [
];
public $search = '';
public $active_mailing = null;
public function mount(Project $project)
{
$this->project = $project;
$this->resetFormData();
}
public function showMailingDetailsModal(ProjectMailing $mailing)
{
$this->formData = $mailing->toArray();
$this->formData['attachments'] = $mailing->attachments->toArray();
$this->formData['recipients'] = $mailing->recipients->toArray();
$this->mailingDetailsModal = true;
}
public function closeMailingDetailsModal()
{
$this->mailingDetailsModal = false;
$this->resetFormData();
}
private function resetFormData()
{
$this->formData = [
'subject' => '',
'message' => '',
'created_at' => '',
'attachments' => [],
'recipients' => [],
];
}
private function applySearch($query)
{
if ($this->search) {
$query->where('subject', 'like', '%'.$this->search.'%');
}
return $query;
}
public function placeholder()
{
return view('livewire.projects.mailing-manager-placeholder');
}
public function render()
{
$query = $this->project
->mailings()
->orderBy('created_at', 'desc');
$query = $this->applySearch($query);
$mailings = $query->paginate(10, pageName: 'mailingPage');
return view('livewire.projects.tabs.mailings', [
'mailings' => $mailings,
]);
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace App\Livewire\Projects\Tabs;
use App\Jobs\ProjectMosiacGeneratorJob;
use App\Models\Project;
use Livewire\Component;
use Livewire\WithPagination;
class Mosaic extends Component
{
use WithPagination;
public $project;
public $formData = [
'week' => '',
'year' => '',
];
public $showCreateModal = false;
public $search = "";
public function mount(Project $project)
{
$this->path = $project->download_path;
}
public function saveMosaic()
{
$this->validate([
'formData.year' => 'required',
'formData.week' => 'required',
]);
$mosaic = $this->project->mosaics()->updateOrCreate([
'name' => sprintf('Week %s, %s', $this->formData['week'], $this->formData['year']),
'year' => $this->formData['year'],
'week' => $this->formData['week'],
], [
'path' => $this->project->getMosaicPath(),
]);
ProjectMosiacGeneratorJob::dispatch($mosaic);
$this->showCreateModal = false;
}
public function openCreateMosiacsModal()
{
$this->showCreateModal = true;
}
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');
}
private function applySearch($query)
{
if ($this->search) {
$query->where('name', 'like', '%' . $this->search . '%');
$query->orWhere('year', 'like', '%' . $this->search . '%');
$query->orWhere('week', 'like', '%' . $this->search . '%');
}
return $query;
}
public function update($property)
{
if ($property === 'search') {
$this->resetPage('mosaicPage');
}
}
public function placeholder()
{
return view('livewire.projects.mosaic-manager-placeholder');
}
public function render()
{
$query = $this->project->mosaics()
->orderBy('year', 'desc')
->orderBy('week', 'desc');
$query = $this->applySearch($query);
$mosaics = $query->paginate(10, pageName: 'mosaicPage');
return view('livewire.projects.tabs.mosaic', [
'mosaics' => $mosaics
]);
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace App\Livewire\Projects\Tabs;
use App\Jobs\ProjectReportGeneratorJob;
use App\Models\Project;
use App\Models\ProjectReport;
use App\Rules\AllMosaicsPresentRule;
use Livewire\Component;
use Livewire\WithPagination;
class Report extends Component
{
use WithPagination;
public $formData = [];
public $project_id;
public $search = "";
public $showReportModal = false;
public $listeners = ['refresh' => '$refresh'];
public function openCreateReportModal()
{
$this->resetFormData();
$this->showReportModal = true;
}
private function resetFormData()
{
$this->formData['week'] = now()->weekOfYear;
$this->formData['year'] = now()->year;
}
public function saveProjectReport()
{
$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>';
}
// @TODO dit moet gecorrigeerd voor de project settings;
$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');
}
public function deleteReport(ProjectReport $report)
{
$report->deleteMe();
$this->dispatch('refresh');
}
private function applySearch($query)
{
if ($this->search) {
$query->where('name', 'like', '%'.$this->search.'%');
$query->orWhere('year', 'like', '%'.$this->search.'%');
$query->orWhere('week', 'like', '%'.$this->search.'%');
}
return $query;
}
public function placeholder()
{
return view('livewire.projects.report-manager-placeholder');
}
public function render()
{
$query = Project::find($this->project_id)
->reports()
->orderBy('year', 'desc')
->orderBy('week', 'desc');
$query = $this->applySearch($query);
$reports = $query->paginate(10, pageName: 'reportPage');
return view('livewire.projects.tabs.report')
->with(compact('reports'));
}
}

View file

@ -21,7 +21,7 @@
@foreach ($projectManager->projects->sortBy('name') as $project)
<div class="flex items-center justify-between">
<div class="break-all">
<a href="{!! route('project.show', $project->id) !!}">{{ $project->name }}</a>
<a href="{!! route('project.show', $project->name) !!}">{{ $project->name }}</a>
</div>
<div class="flex items-center ml-2">
<button class="cursor-pointer ml-6 text-sm text-gray-500"

View file

@ -1,7 +1,7 @@
<div
@if($project->hasPendingDownload())
wire:poll.1s=""
@endif
{{-- @if($project->hasPendingDownload())--}}
{{-- wire:poll.1s=""--}}
{{-- @endif--}}
>
<div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:flex-col sm:items-center">

View file

@ -0,0 +1,6 @@
<div class="flex flex-col w-fit bg-white gap-2 p-6 rounded-lg shadow">
<a class="text-center uppercase tracking-widest font-semibold text-sm hover:bg-indigo-500 hover:text-white px-4 py-2 rounded-lg {{!($activeTab == 'downloads') ?: 'bg-indigo-600 text-white'}}" href="{{route('project.show',[$projectName,'downloads'])}}">Downloads</a>
<a class="text-center uppercase tracking-widest font-semibold text-sm hover:bg-indigo-500 hover:text-white px-4 py-2 rounded-lg {{!($activeTab == 'mosaics') ?: 'bg-indigo-600 text-white'}}" href="{{route('project.show',[$projectName,'mosaics'])}}">Mosaics</a>
<a class="text-center uppercase tracking-widest font-semibold text-sm hover:bg-indigo-500 hover:text-white px-4 py-2 rounded-lg {{!($activeTab == 'reports') ?: 'bg-indigo-600 text-white'}}" href="{{route('project.show',[$projectName,'reports'])}}">Reports</a>
<a class="text-center uppercase tracking-widest font-semibold text-sm hover:bg-indigo-500 hover:text-white px-4 py-2 rounded-lg {{!($activeTab == 'mailings') ?: 'bg-indigo-600 text-white' }}" href="{{route('project.show',[$projectName,'mailings'])}}">Mailings</a>
</div>

View file

@ -0,0 +1,136 @@
<div>
<div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:flex-col sm:items-center">
<div class="w-full">
<h1 class="text-base font-semibold leading-6 text-gray-900">{{ __('Mailing') }}</h1>
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:mt-0 sm:flex sm:justify-between w-full">
<x-search></x-search>
</div>
</div>
<div class="mt-8 ">
<div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle mb-10">
<div class="">
<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">
Id
</th>
<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">@lang('Subject')</th>
<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">@lang('Status')</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">#</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">@lang('Attachment')
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($mailings as $mail)
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">{{ $mail->id }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ $mail->subject }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<x-badge :status="$mail->status"></x-badge>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ $mail->recipients()->count() }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ $mail->attachments()->pluck('name')->join( ', ') }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<button type="button" wire:click="showMailingDetailsModal({{ $mail->id }})"
class="text-indigo-600 hover:text-indigo-900">Show
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="absolute inset-0 bg-white opacity-75" wire:loading></div>
</div>
</div>
<div class="pt-4 flex justify-between items-center">
<div class="text-gray-700 text-sm">
Results: {{ \Illuminate\Support\Number::format($mailings->total()) }}
</div>
{{ $mailings->links('livewire.pagination') }}
</div>
<x-modal wire:model.live="mailingDetailsModal">
<x-form-modal submit="saveProject">
<x-slot name="title">
{{ __('Mailing') }}
</x-slot>
<x-slot name="description">
<x-label for="created_at" value="{{ __('Created') }}"/>
<x-label id="created_at"
value="{{ \Carbon\Carbon::parse($formData['created_at'])->format('Y-m-d H:i') }}"/>
</x-slot>
<x-slot name="form">
<div class="col-span-6">
<x-label for="recipients" value="{{ __('Recipients') }}"/>
@foreach($formData['recipients'] as $key => $recipient)
<div class="col-span-6 sm:col-span-4">
<x-label class="inline-block" for="recipients"
value="{{ $recipient['name'] }}"/>
<x-label class="inline-block" for="recipients"
value="<{{ $recipient['email'] }}>"/>
</div>
@endforeach
</div>
<div class="col-span-6">
<x-label for="subject" value="{{ __('Subject') }}"/>
<x-input id="subject" type="text" class="mt-1 block w-full" disabled
wire:model="formData.subject"/>
</div>
<div class="col-span-6">
<x-label for="message" value="{{ __('Message') }}"/>
<textarea
id="message"
type="text"
class="mt-1 block w-full"
wire:model="formData.message"
disabled
></textarea>
</div>
@empty($formData['attachments'])
<div class="col-span-6">
<x-label for="message" value="{{ __('Attachment') }}"/>
<x-label class="inline-block" for=""
value="{{ __('No attachments where send with this message.') }}"/>
</div>
@else
@foreach($formData['attachments'] as $key => $attachment)
<div class="col-span-6">
<x-label class="inline-block" for="recipients"
value="{{ $attachment['name'] }}"/>
</div>
@endforeach
@endempty
</x-slot>
<x-slot name="actions">
<x-secondary-button class="mr-3"
type="button"
wire:click="closeMailingDetailsModal"
>
{{ __('Close') }}
</x-secondary-button>
</x-slot>
</x-form-modal>
</x-modal>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,73 @@
<div
@if($project->hasPendingMosaic())
wire:poll.1s=""
@endif
>
<div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:flex-col sm:items-center">
<div class="flex justify-between w-full my-4">
<h1 class="text-base font-semibold leading-6 text-gray-900">Mosaics</h1>
@if ($project->hasPendingDownload())
<p class="text-base text-gray-700 animate-pulse">
Pending mosaics for this project: {{ $project->mosaics()->statusPending()->count() }}
</p>
@endif
</div>
<div class="mt-4 sm:mt-0 sm:flex sm:justify-between w-full">
<x-search></x-search>
<x-button wire:click="openCreateMosiacsModal"
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 Mosaic
</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="relative">
<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">
Year-Week
</th>
<th scope="col"
class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900 sm:pr-8 lg:pr-8">
Status
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
@foreach($mosaics as $mosaic)
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">{{ $mosaic->name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ $mosaic->year }}-{{ $mosaic->week}}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
<x-badge :status="$mosaic->status"></x-badge>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="pt-4 flex justify-between items-center">
<div class="text-gray-700 text-sm">
Results: {{ \Illuminate\Support\Number::format($mosaics->total()) }}
</div>
{{ $mosaics->links('livewire.pagination') }}
</div>
<div class="absolute inset-0 bg-white opacity-75" wire:loading></div>
</div>
</div>
</div>
</div>
<x-mosaic-create-modal :manager="$this"/>
</div>

View file

@ -0,0 +1,59 @@
<div>
<div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:flex-col sm:items-center">
<div class="w-full">
<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:mt-0 sm:flex sm:justify-between w-full">
<x-search></x-search>
<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="relative">
<div class="inline-block min-w-full py-2 align-middle mb-10">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Created At
</th>
<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="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($reports as $report)
<livewire:projects.report-row :$report :key="$report->id"/>
@endforeach
</tbody>
</table>
</div>
<div class="pt-4 flex justify-between items-center">
<div class="text-gray-700 text-sm">
Results: {{ \Illuminate\Support\Number::format($reports->total()) }}
</div>
{{ $reports->links('livewire.pagination') }}
</div>
<div class="absolute inset-0 bg-white opacity-75" wire:loading></div>
</div>
</div>
</div>
</div>
<x-report-manager-properties-modal :reportManager="$this"/>
</div>

View file

@ -1,48 +1,29 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Project') }} {{ $project->name }}
{{ __('Project') }} {{ $project->name}}
</h2>
</x-slot>
<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="mosaics"
: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"
>{{ __('Mosaics') }}</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>
<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"
>{{ __('Mailings') }}</button>
<div id="main" class="flex">
<livewire:projects.menu :project-name="$project->name" :active-tab="$currentTab"></livewire:projects.menu>
<div class="flex-1 p-4 ml-2 rounded-lg bg-white shadow">
@switch($currentTab)
@case('downloads')
<livewire:projects.tabs.download :project="$project"></livewire:projects.tabs.download>
@break
@case('mosaics')
<livewire:projects.tabs.mosaic :project="$project"></livewire:projects.tabs.mosaic>
@break
@case('reports')
<livewire:projects.tabs.report :project_id="$project->id"></livewire:projects.tabs.report>
@break
@case('mailings')
<livewire:projects.tabs.mailings :project="$project"></livewire:projects.tabs.mailings>
@break
@default
<div class="flex w-full h-screen justify-center items-center"> Menu Component not found.</div>
@endswitch
</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>
<section x-tabs:panel class="p-8">
<livewire:projects.mosaic-manager :project="$project" />
</section>
<section x-tabs:panel class="p-8">
<livewire:projects.report-manager :project="$project" />
</section>
<section x-tabs:panel class="p-8">
<livewire:projects.mailing-manager :project="$project" />
</section>
</div>
</div>
</x-app-layout>

View file

@ -29,7 +29,7 @@
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/projects/{project}/{currentTab?}', [\App\Http\Controllers\ProjectController::class, 'show'])->name('project.show');
Route::get('/projects/{projectName}/{currentTab?}', [\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');
});