phase 5
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['name' => 'CRM'];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration { public function up(): void {
|
||||
Schema::create('crm_companies', function(Blueprint $t): void { $t->id(); $t->string('name'); $t->string('website')->nullable(); $t->string('phone')->nullable(); $t->string('email')->nullable(); $t->string('industry')->nullable(); $t->text('address')->nullable(); $t->text('notes')->nullable(); $t->boolean('is_active')->default(true); $t->timestamps(); });
|
||||
Schema::create('crm_contacts', function(Blueprint $t): void { $t->id(); $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete(); $t->string('first_name'); $t->string('last_name'); $t->string('email')->nullable()->index(); $t->string('phone')->nullable(); $t->string('job_title')->nullable(); $t->text('notes')->nullable(); $t->boolean('is_active')->default(true); $t->timestamps(); });
|
||||
Schema::create('crm_leads', function(Blueprint $t): void { $t->id(); $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete(); $t->string('title'); $t->string('source')->nullable(); $t->string('status')->default('new')->index(); $t->decimal('estimated_value',12,2)->nullable(); $t->text('notes')->nullable(); $t->timestamp('next_action_at')->nullable(); $t->timestamps(); });
|
||||
Schema::create('crm_opportunities', function(Blueprint $t): void { $t->id(); $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete(); $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete(); $t->string('name'); $t->string('stage')->default('qualification')->index(); $t->decimal('value',12,2)->default(0); $t->unsignedTinyInteger('probability')->default(0); $t->date('expected_close_at')->nullable(); $t->text('notes')->nullable(); $t->timestamps(); });
|
||||
Schema::create('crm_activities', function(Blueprint $t): void { $t->id(); $t->nullableMorphs('subject'); $t->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $t->string('type'); $t->string('title'); $t->text('description')->nullable(); $t->timestamp('due_at')->nullable(); $t->timestamp('completed_at')->nullable(); $t->timestamps(); });
|
||||
} public function down(): void { Schema::dropIfExists('crm_activities'); Schema::dropIfExists('crm_opportunities'); Schema::dropIfExists('crm_leads'); Schema::dropIfExists('crm_contacts'); Schema::dropIfExists('crm_companies'); } };
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Modules\CRM\Models\Company; use Modules\CRM\Models\Contact; use Modules\CRM\Models\Lead; use Modules\CRM\Models\Opportunity;
|
||||
final class CRMController extends Controller {
|
||||
public function index(): View { return view('crm::admin.index',['companies'=>Company::withCount('contacts')->latest()->paginate(20),'contacts'=>Contact::with('company')->latest()->limit(10)->get(),'leads'=>Lead::with('contact')->latest()->limit(10)->get(),'opportunities'=>Opportunity::with('company')->latest()->limit(10)->get(),'pipelineValue'=>Opportunity::whereNotIn('stage',['won','lost'])->sum('value')]); }
|
||||
public function storeCompany(Request $r): RedirectResponse { Company::create($r->validate(['name'=>['required','string','max:255'],'website'=>['nullable','url','max:255'],'phone'=>['nullable','string','max:80'],'email'=>['nullable','email','max:255'],'industry'=>['nullable','string','max:120'],'address'=>['nullable','string'],'notes'=>['nullable','string']])+['is_active'=>$r->boolean('is_active',true)]); return back()->with('status','Company created.'); }
|
||||
public function storeContact(Request $r): RedirectResponse { Contact::create($r->validate(['company_id'=>['nullable','exists:crm_companies,id'],'first_name'=>['required','string','max:120'],'last_name'=>['required','string','max:120'],'email'=>['nullable','email','max:255'],'phone'=>['nullable','string','max:80'],'job_title'=>['nullable','string','max:120'],'notes'=>['nullable','string']])+['is_active'=>$r->boolean('is_active',true)]); return back()->with('status','Contact created.'); }
|
||||
public function storeLead(Request $r): RedirectResponse { Lead::create($r->validate(['contact_id'=>['nullable','exists:crm_contacts,id'],'title'=>['required','string','max:255'],'source'=>['nullable','string','max:120'],'status'=>['required','in:new,qualified,proposal,won,lost'],'estimated_value'=>['nullable','numeric','min:0'],'notes'=>['nullable','string'],'next_action_at'=>['nullable','date']])); return back()->with('status','Lead created.'); }
|
||||
public function storeOpportunity(Request $r): RedirectResponse { Opportunity::create($r->validate(['company_id'=>['nullable','exists:crm_companies,id'],'contact_id'=>['nullable','exists:crm_contacts,id'],'name'=>['required','string','max:255'],'stage'=>['required','in:qualification,proposal,negotiation,won,lost'],'value'=>['required','numeric','min:0'],'probability'=>['required','integer','between:0,100'],'expected_close_at'=>['nullable','date'],'notes'=>['nullable','string']])); return back()->with('status','Opportunity created.'); }
|
||||
public function destroyCompany(Company $company): RedirectResponse { $company->delete(); return back()->with('status','Company deleted.'); }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
final class Activity extends Model { protected $table='crm_activities'; protected $fillable=['subject_type','subject_id','user_id','type','title','description','due_at','completed_at']; protected function casts(): array { return ['due_at'=>'datetime','completed_at'=>'datetime']; } public function subject(): MorphTo { return $this->morphTo(); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class Company extends Model { protected $table='crm_companies'; protected $fillable=['name','website','phone','email','industry','address','notes','is_active']; protected function casts(): array { return ['is_active'=>'boolean']; } public function contacts(): HasMany { return $this->hasMany(Contact::class); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
final class Contact extends Model { protected $table='crm_contacts'; protected $fillable=['company_id','first_name','last_name','email','phone','job_title','notes','is_active']; protected function casts(): array { return ['is_active'=>'boolean']; } public function company(): BelongsTo { return $this->belongsTo(Company::class); } public function getNameAttribute(): string { return trim($this->first_name.' '.$this->last_name); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
final class Lead extends Model { protected $table='crm_leads'; protected $fillable=['contact_id','title','source','status','estimated_value','notes','next_action_at']; protected function casts(): array { return ['estimated_value'=>'decimal:2','next_action_at'=>'datetime']; } public function contact(): BelongsTo { return $this->belongsTo(Contact::class); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
final class Opportunity extends Model { protected $table='crm_opportunities'; protected $fillable=['company_id','contact_id','name','stage','value','probability','expected_close_at','notes']; protected function casts(): array { return ['value'=>'decimal:2','expected_close_at'=>'date']; } public function company(): BelongsTo { return $this->belongsTo(Company::class); } public function contact(): BelongsTo { return $this->belongsTo(Contact::class); } }
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
final class CRMServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('CRM', 'Config/config.php'), 'crm');
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('CRM', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('CRM', 'Resources/views'), 'crm');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\CRM\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('CRM', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('CRM')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">CRM</h1><p class="text-sm text-zinc-500">Companies, contacts, leads and sales opportunities.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<div class="grid gap-4 md:grid-cols-4"><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Companies</div><div class="text-2xl font-semibold">{{ $companies->total() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Contacts</div><div class="text-2xl font-semibold">{{ \Modules\CRM\Models\Contact::count() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Open leads</div><div class="text-2xl font-semibold">{{ \Modules\CRM\Models\Lead::whereNotIn('status',['won','lost'])->count() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Pipeline</div><div class="text-2xl font-semibold">£{{ number_format($pipelineValue,2) }}</div></div></div><div class="grid gap-6 lg:grid-cols-2"><form method="POST" action="{{ route('administration.crm.companies.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add company</h2><flux:input name="name" label="Company name" required/><div class="grid gap-4 md:grid-cols-2"><flux:input name="email" type="email" label="Email"/><flux:input name="phone" label="Phone"/><flux:input name="website" label="Website"/><flux:input name="industry" label="Industry"/></div><flux:textarea name="address" label="Address"/><flux:button type="submit" variant="primary">Create company</flux:button></form><form method="POST" action="{{ route('administration.crm.contacts.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add contact</h2><flux:select name="company_id" label="Company"><option value="">None</option>@foreach($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</flux:select><div class="grid gap-4 md:grid-cols-2"><flux:input name="first_name" label="First name" required/><flux:input name="last_name" label="Last name" required/><flux:input name="email" type="email" label="Email"/><flux:input name="phone" label="Phone"/></div><flux:button type="submit" variant="primary">Create contact</flux:button></form></div><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Company</th><th class="p-3 text-left">Industry</th><th class="p-3 text-left">Contacts</th><th class="p-3"></th></tr></thead><tbody>@forelse($companies as $company)<tr class="border-t"><td class="p-3 font-medium">{{ $company->name }}</td><td class="p-3">{{ $company->industry ?: '—' }}</td><td class="p-3">{{ $company->contacts_count }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.crm.companies.destroy',$company) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="4" class="p-6 text-center text-zinc-500">No companies yet.</td></tr>@endforelse</tbody></table></div>{{ $companies->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route; use Modules\CRM\Http\Controllers\CRMController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/crm')->name('administration.crm.')->group(function(): void { Route::get('/',[CRMController::class,'index'])->name('index'); Route::post('/companies',[CRMController::class,'storeCompany'])->name('companies.store'); Route::delete('/companies/{company}',[CRMController::class,'destroyCompany'])->name('companies.destroy'); Route::post('/contacts',[CRMController::class,'storeContact'])->name('contacts.store'); Route::post('/leads',[CRMController::class,'storeLead'])->name('leads.store'); Route::post('/opportunities',[CRMController::class,'storeOpportunity'])->name('opportunities.store'); });
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Blog\Tests\Feature;
|
||||
namespace Modules\CRM\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Blog', config('blog.name'));
|
||||
$this->assertSame('CRM', config('crm.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "CRM",
|
||||
"alias": "crm",
|
||||
"version": "1.0.0",
|
||||
"description": "Customer relationship management for companies, contacts, leads, opportunities and activities.",
|
||||
"keywords": ["business", "crm"],
|
||||
"priority": 40,
|
||||
"providers": ["Modules\\CRM\\Providers\\CRMServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['name' => 'Careers'];
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration { public function up(): void { Schema::create('career_jobs',function(Blueprint $t): void{$t->id();$t->string('title');$t->string('slug')->unique();$t->string('department')->nullable();$t->string('location')->nullable();$t->string('employment_type')->default('full-time');$t->longText('description')->nullable();$t->string('status')->default('draft')->index();$t->timestamp('published_at')->nullable();$t->timestamp('closes_at')->nullable();$t->timestamps();}); Schema::create('career_applications',function(Blueprint $t): void{$t->id();$t->foreignId('job_id')->constrained('career_jobs')->cascadeOnDelete();$t->string('name');$t->string('email')->index();$t->string('phone')->nullable();$t->string('cv_path')->nullable();$t->text('cover_letter')->nullable();$t->string('status')->default('new')->index();$t->text('notes')->nullable();$t->timestamps();}); } public function down(): void { Schema::dropIfExists('career_applications');Schema::dropIfExists('career_jobs'); } };
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Modules\Careers\Models\Job; use Modules\Careers\Models\Application;
|
||||
final class CareerController extends Controller { public function index(): View { return view('careers::admin.index',['jobs'=>Job::withCount('applications')->latest()->paginate(20),'applications'=>Application::latest()->limit(10)->get()]); } public function store(Request $r): RedirectResponse { Job::create($r->validate(['title'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:career_jobs,slug'],'department'=>['nullable','string','max:120'],'location'=>['nullable','string','max:120'],'employment_type'=>['required','in:full-time,part-time,contract,temporary,internship'],'description'=>['nullable','string'],'status'=>['required','in:draft,published,closed'],'published_at'=>['nullable','date'],'closes_at'=>['nullable','date','after:published_at']])); return back()->with('status','Job created.'); } public function destroy(Job $job): RedirectResponse { $job->delete(); return back()->with('status','Job deleted.'); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Application extends Model { protected $table='career_applications'; protected $fillable=['job_id','name','email','phone','cv_path','cover_letter','status','notes']; }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class Job extends Model { protected $table='career_jobs'; protected $fillable=['title','slug','department','location','employment_type','description','status','published_at','closes_at']; protected function casts(): array { return ['published_at'=>'datetime','closes_at'=>'datetime']; } public function applications(): HasMany { return $this->hasMany(Application::class); } }
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
final class CareersServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Careers', 'Config/config.php'), 'careers');
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Careers', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Careers', 'Resources/views'), 'careers');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Careers', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Careers')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Careers</h1><p class="text-sm text-zinc-500">Manage vacancies and candidate applications.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.careers.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Create vacancy</h2><div class="grid gap-4 md:grid-cols-2"><flux:input name="title" label="Job title" required/><flux:input name="slug" label="Slug" required/><flux:input name="department" label="Department"/><flux:input name="location" label="Location"/><flux:select name="employment_type" label="Employment type"><option value="full-time">Full time</option><option value="part-time">Part time</option><option value="contract">Contract</option><option value="temporary">Temporary</option><option value="internship">Internship</option></flux:select><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="published">Published</option><option value="closed">Closed</option></flux:select></div><flux:textarea name="description" label="Description" rows="8"/><flux:button type="submit" variant="primary">Create vacancy</flux:button></form><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Vacancy</th><th class="p-3 text-left">Department</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Applications</th><th class="p-3"></th></tr></thead><tbody>@forelse($jobs as $job)<tr class="border-t"><td class="p-3 font-medium">{{ $job->title }}</td><td class="p-3">{{ $job->department ?: '—' }}</td><td class="p-3">{{ ucfirst($job->status) }}</td><td class="p-3">{{ $job->applications_count }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.careers.destroy',$job) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No vacancies yet.</td></tr>@endforelse</tbody></table></div>{{ $jobs->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route; use Modules\Careers\Http\Controllers\CareerController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/careers')->name('administration.careers.')->group(function(): void { Route::get('/',[CareerController::class,'index'])->name('index'); Route::post('/',[CareerController::class,'store'])->name('store'); Route::delete('/{job}',[CareerController::class,'destroy'])->name('destroy'); });
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Careers\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Careers', config('careers.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Careers",
|
||||
"alias": "careers",
|
||||
"version": "1.0.0",
|
||||
"description": "Job vacancies and candidate application workflow.",
|
||||
"keywords": ["business", "careers"],
|
||||
"priority": 43,
|
||||
"providers": ["Modules\\Careers\\Providers\\CareersServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['name' => 'Events'];
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration { public function up(): void { Schema::create('event_venues',function(Blueprint $t): void{$t->id();$t->string('name');$t->text('address')->nullable();$t->unsignedInteger('capacity')->nullable();$t->timestamps();}); Schema::create('events',function(Blueprint $t): void{$t->id();$t->foreignId('venue_id')->nullable()->constrained('event_venues')->nullOnDelete();$t->string('title');$t->string('slug')->unique();$t->text('summary')->nullable();$t->longText('description')->nullable();$t->timestamp('starts_at')->index();$t->timestamp('ends_at')->nullable();$t->string('status')->default('draft')->index();$t->unsignedInteger('capacity')->nullable();$t->decimal('price',10,2)->default(0);$t->boolean('registration_open')->default(false);$t->timestamps();}); Schema::create('event_registrations',function(Blueprint $t): void{$t->id();$t->foreignId('event_id')->constrained()->cascadeOnDelete();$t->string('name');$t->string('email')->index();$t->unsignedInteger('quantity')->default(1);$t->string('status')->default('confirmed')->index();$t->text('notes')->nullable();$t->timestamps();}); } public function down(): void { Schema::dropIfExists('event_registrations');Schema::dropIfExists('events');Schema::dropIfExists('event_venues'); } };
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Events\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Modules\Events\Models\Event; use Modules\Events\Models\Venue;
|
||||
final class EventController extends Controller { public function index(): View { return view('events::admin.index',['events'=>Event::with(['venue'])->withCount('registrations')->orderBy('starts_at')->paginate(20),'venues'=>Venue::orderBy('name')->get()]); } public function store(Request $r): RedirectResponse { Event::create($r->validate(['venue_id'=>['nullable','exists:event_venues,id'],'title'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:events,slug'],'summary'=>['nullable','string'],'description'=>['nullable','string'],'starts_at'=>['required','date'],'ends_at'=>['nullable','date','after:starts_at'],'status'=>['required','in:draft,published,cancelled,completed'],'capacity'=>['nullable','integer','min:1'],'price'=>['required','numeric','min:0']])+['registration_open'=>$r->boolean('registration_open')]); return back()->with('status','Event created.'); } public function destroy(Event $event): RedirectResponse { $event->delete(); return back()->with('status','Event deleted.'); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Events\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class Event extends Model { protected $fillable=['venue_id','title','slug','summary','description','starts_at','ends_at','status','capacity','price','registration_open']; protected function casts(): array { return ['starts_at'=>'datetime','ends_at'=>'datetime','price'=>'decimal:2','registration_open'=>'boolean']; } public function venue(): BelongsTo { return $this->belongsTo(Venue::class); } public function registrations(): HasMany { return $this->hasMany(Registration::class); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Events\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Registration extends Model { protected $table='event_registrations'; protected $fillable=['event_id','name','email','quantity','status','notes']; }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Events\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Venue extends Model { protected $table='event_venues'; protected $fillable=['name','address','capacity']; }
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\Events\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
final class EventsServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Events', 'Config/config.php'), 'events');
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Events', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Events', 'Resources/views'), 'events');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Events\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Events', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Events')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Events</h1><p class="text-sm text-zinc-500">Manage events, venues and registrations.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.events.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Create event</h2><div class="grid gap-4 md:grid-cols-2"><flux:input name="title" label="Title" required/><flux:input name="slug" label="Slug" required/><flux:input type="datetime-local" name="starts_at" label="Starts" required/><flux:input type="datetime-local" name="ends_at" label="Ends"/><flux:input type="number" min="0" step="0.01" name="price" label="Price" value="0" required/><flux:input type="number" min="1" name="capacity" label="Capacity"/><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="published">Published</option><option value="cancelled">Cancelled</option></flux:select></div><flux:textarea name="summary" label="Summary"/><flux:checkbox name="registration_open" value="1" label="Registration open"/><flux:button type="submit" variant="primary">Create event</flux:button></form><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Event</th><th class="p-3 text-left">Starts</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Registrations</th><th class="p-3"></th></tr></thead><tbody>@forelse($events as $event)<tr class="border-t"><td class="p-3 font-medium">{{ $event->title }}</td><td class="p-3">{{ $event->starts_at->format('j M Y H:i') }}</td><td class="p-3">{{ ucfirst($event->status) }}</td><td class="p-3">{{ $event->registrations_count }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.events.destroy',$event) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No events yet.</td></tr>@endforelse</tbody></table></div>{{ $events->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route; use Modules\Events\Http\Controllers\EventController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/events')->name('administration.events.')->group(function(): void { Route::get('/',[EventController::class,'index'])->name('index'); Route::post('/',[EventController::class,'store'])->name('store'); Route::delete('/{event}',[EventController::class,'destroy'])->name('destroy'); });
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Events\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Events', config('events.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"alias": "events",
|
||||
"version": "1.0.0",
|
||||
"description": "Events, venues and registration management.",
|
||||
"keywords": ["business", "events"],
|
||||
"priority": 42,
|
||||
"providers": ["Modules\\Events\\Providers\\EventsServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['name' => 'Locations'];
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration { public function up(): void { Schema::create('locations',function(Blueprint $t): void{$t->id();$t->string('name');$t->string('slug')->unique();$t->string('type')->default('office');$t->text('address')->nullable();$t->string('postcode')->nullable();$t->string('country')->nullable();$t->string('phone')->nullable();$t->string('email')->nullable();$t->decimal('latitude',10,7)->nullable();$t->decimal('longitude',10,7)->nullable();$t->json('opening_hours')->nullable();$t->boolean('is_active')->default(true);$t->unsignedInteger('sort_order')->default(0);$t->timestamps();}); } public function down(): void { Schema::dropIfExists('locations'); } };
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Locations\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Modules\Locations\Models\Location;
|
||||
final class LocationController extends Controller { public function index(): View { return view('locations::admin.index',['locations'=>Location::orderBy('sort_order')->orderBy('name')->paginate(25)]); } public function store(Request $r): RedirectResponse { Location::create($r->validate(['name'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:locations,slug'],'type'=>['required','in:office,store,warehouse,venue,service'],'address'=>['nullable','string'],'postcode'=>['nullable','string','max:30'],'country'=>['nullable','string','max:120'],'phone'=>['nullable','string','max:80'],'email'=>['nullable','email','max:255'],'latitude'=>['nullable','numeric','between:-90,90'],'longitude'=>['nullable','numeric','between:-180,180'],'sort_order'=>['nullable','integer','min:0']])+['is_active'=>$r->boolean('is_active',true)]); return back()->with('status','Location created.'); } public function destroy(Location $location): RedirectResponse { $location->delete(); return back()->with('status','Location deleted.'); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Locations\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Location extends Model { protected $fillable=['name','slug','type','address','postcode','country','phone','email','latitude','longitude','opening_hours','is_active','sort_order']; protected function casts(): array { return ['latitude'=>'decimal:7','longitude'=>'decimal:7','opening_hours'=>'array','is_active'=>'boolean']; } }
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\Locations\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
final class LocationsServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Locations', 'Config/config.php'), 'locations');
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Locations', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Locations', 'Resources/views'), 'locations');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Locations\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Locations', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Locations')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Locations</h1><p class="text-sm text-zinc-500">Manage offices, stores, venues and service locations.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.locations.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add location</h2><div class="grid gap-4 md:grid-cols-2"><flux:input name="name" label="Name" required/><flux:input name="slug" label="Slug" required/><flux:select name="type" label="Type"><option value="office">Office</option><option value="store">Store</option><option value="warehouse">Warehouse</option><option value="venue">Venue</option><option value="service">Service</option></flux:select><flux:input name="postcode" label="Postcode"/><flux:input name="phone" label="Phone"/><flux:input name="email" type="email" label="Email"/></div><flux:textarea name="address" label="Address"/><flux:checkbox name="is_active" value="1" label="Active" checked/><flux:button type="submit" variant="primary">Create location</flux:button></form><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Location</th><th class="p-3 text-left">Type</th><th class="p-3 text-left">Contact</th><th class="p-3 text-left">Status</th><th class="p-3"></th></tr></thead><tbody>@forelse($locations as $location)<tr class="border-t"><td class="p-3"><div class="font-medium">{{ $location->name }}</div><div class="text-zinc-500">{{ $location->address }}</div></td><td class="p-3">{{ ucfirst($location->type) }}</td><td class="p-3">{{ $location->phone ?: $location->email ?: '—' }}</td><td class="p-3">{{ $location->is_active ? 'Active' : 'Inactive' }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.locations.destroy',$location) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No locations yet.</td></tr>@endforelse</tbody></table></div>{{ $locations->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route; use Modules\Locations\Http\Controllers\LocationController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/locations')->name('administration.locations.')->group(function(): void { Route::get('/',[LocationController::class,'index'])->name('index'); Route::post('/',[LocationController::class,'store'])->name('store'); Route::delete('/{location}',[LocationController::class,'destroy'])->name('destroy'); });
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Locations\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Locations', config('locations.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Locations",
|
||||
"alias": "locations",
|
||||
"version": "1.0.0",
|
||||
"description": "Office and service location management.",
|
||||
"keywords": ["business", "locations"],
|
||||
"priority": 44,
|
||||
"providers": ["Modules\\Locations\\Providers\\LocationsServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['name' => 'Newsletter'];
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration { public function up(): void { Schema::create('newsletter_lists',function(Blueprint $t): void{$t->id();$t->string('name');$t->text('description')->nullable();$t->boolean('is_active')->default(true);$t->timestamps();}); Schema::create('newsletter_subscribers',function(Blueprint $t): void{$t->id();$t->string('email')->unique();$t->string('name')->nullable();$t->string('status')->default('pending')->index();$t->string('confirmation_token')->nullable()->unique();$t->timestamp('confirmed_at')->nullable();$t->timestamp('unsubscribed_at')->nullable();$t->json('metadata')->nullable();$t->timestamps();}); Schema::create('newsletter_list_subscriber',function(Blueprint $t): void{$t->foreignId('list_id')->constrained('newsletter_lists')->cascadeOnDelete();$t->foreignId('subscriber_id')->constrained('newsletter_subscribers')->cascadeOnDelete();$t->primary(['list_id','subscriber_id']);}); Schema::create('newsletter_campaigns',function(Blueprint $t): void{$t->id();$t->foreignId('list_id')->nullable()->constrained('newsletter_lists')->nullOnDelete();$t->string('name');$t->string('subject');$t->longText('content')->nullable();$t->string('status')->default('draft')->index();$t->timestamp('scheduled_at')->nullable();$t->timestamp('sent_at')->nullable();$t->unsignedInteger('sent_count')->default(0);$t->unsignedInteger('open_count')->default(0);$t->unsignedInteger('click_count')->default(0);$t->timestamps();}); } public function down(): void { Schema::dropIfExists('newsletter_campaigns');Schema::dropIfExists('newsletter_list_subscriber');Schema::dropIfExists('newsletter_subscribers');Schema::dropIfExists('newsletter_lists'); } };
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Illuminate\Support\Str; use Modules\Newsletter\Models\Campaign; use Modules\Newsletter\Models\MailingList; use Modules\Newsletter\Models\Subscriber;
|
||||
final class NewsletterController { public function index(): View { return view('newsletter::admin.index',['subscribers'=>Subscriber::latest()->paginate(20),'campaigns'=>Campaign::latest()->limit(10)->get(),'lists'=>MailingList::orderBy('name')->get()]); } public function storeSubscriber(Request $r): RedirectResponse { $d=$r->validate(['email'=>['required','email','unique:newsletter_subscribers,email'],'name'=>['nullable','string','max:255']]); Subscriber::create($d+['status'=>'confirmed','confirmation_token'=>Str::random(48),'confirmed_at'=>now()]); return back()->with('status','Subscriber added.'); } public function storeCampaign(Request $r): RedirectResponse { Campaign::create($r->validate(['list_id'=>['nullable','exists:newsletter_lists,id'],'name'=>['required','string','max:255'],'subject'=>['required','string','max:255'],'content'=>['nullable','string'],'status'=>['required','in:draft,scheduled,sent'],'scheduled_at'=>['nullable','date']])); return back()->with('status','Campaign created.'); } public function destroySubscriber(Subscriber $subscriber): RedirectResponse { $subscriber->delete(); return back()->with('status','Subscriber deleted.'); } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Campaign extends Model { protected $table='newsletter_campaigns'; protected $fillable=['list_id','name','subject','content','status','scheduled_at','sent_at','sent_count','open_count','click_count']; protected function casts(): array { return ['scheduled_at'=>'datetime','sent_at'=>'datetime']; } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class MailingList extends Model { protected $table='newsletter_lists'; protected $fillable=['name','description','is_active']; protected function casts(): array { return ['is_active'=>'boolean']; } }
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Models; use Illuminate\Database\Eloquent\Model;
|
||||
final class Subscriber extends Model { protected $table='newsletter_subscribers'; protected $fillable=['email','name','status','confirmation_token','confirmed_at','unsubscribed_at','metadata']; protected function casts(): array { return ['confirmed_at'=>'datetime','unsubscribed_at'=>'datetime','metadata'=>'array']; } }
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
final class NewsletterServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Newsletter', 'Config/config.php'), 'newsletter');
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Newsletter', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Newsletter', 'Resources/views'), 'newsletter');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Newsletter', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Newsletter')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Newsletter</h1><p class="text-sm text-zinc-500">Manage subscribers and email campaigns.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<div class="grid gap-6 lg:grid-cols-2"><form method="POST" action="{{ route('administration.newsletter.subscribers.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add subscriber</h2><flux:input name="email" type="email" label="Email" required/><flux:input name="name" label="Name"/><flux:button type="submit" variant="primary">Add subscriber</flux:button></form><form method="POST" action="{{ route('administration.newsletter.campaigns.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Create campaign</h2><flux:input name="name" label="Campaign name" required/><flux:input name="subject" label="Subject" required/><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="scheduled">Scheduled</option></flux:select><flux:textarea name="content" label="Content"/><flux:button type="submit" variant="primary">Create campaign</flux:button></form></div><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Subscriber</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Confirmed</th><th class="p-3"></th></tr></thead><tbody>@forelse($subscribers as $subscriber)<tr class="border-t"><td class="p-3"><div class="font-medium">{{ $subscriber->email }}</div><div class="text-zinc-500">{{ $subscriber->name }}</div></td><td class="p-3">{{ ucfirst($subscriber->status) }}</td><td class="p-3">{{ optional($subscriber->confirmed_at)->format('j M Y') ?: '—' }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.newsletter.subscribers.destroy',$subscriber) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="4" class="p-6 text-center text-zinc-500">No subscribers yet.</td></tr>@endforelse</tbody></table></div>{{ $subscribers->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route; use Modules\Newsletter\Http\Controllers\NewsletterController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/newsletter')->name('administration.newsletter.')->group(function(): void { Route::get('/',[NewsletterController::class,'index'])->name('index'); Route::post('/subscribers',[NewsletterController::class,'storeSubscriber'])->name('subscribers.store'); Route::delete('/subscribers/{subscriber}',[NewsletterController::class,'destroySubscriber'])->name('subscribers.destroy'); Route::post('/campaigns',[NewsletterController::class,'storeCampaign'])->name('campaigns.store'); });
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Newsletter\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Newsletter', config('newsletter.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Newsletter",
|
||||
"alias": "newsletter",
|
||||
"version": "1.0.0",
|
||||
"description": "Subscriber lists and email campaign management.",
|
||||
"keywords": ["business", "newsletter"],
|
||||
"priority": 41,
|
||||
"providers": ["Modules\\Newsletter\\Providers\\NewsletterServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
@@ -45,6 +45,11 @@ final class RolePermissionSeeder extends Seeder
|
||||
'tags.manage',
|
||||
'authors.manage',
|
||||
'comments.moderate',
|
||||
'crm.manage',
|
||||
'newsletter.manage',
|
||||
'events.manage',
|
||||
'careers.manage',
|
||||
'locations.manage',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
+32
-3
@@ -1,5 +1,34 @@
|
||||
{
|
||||
"Stylist": true, "Core": true, "Administration": true, "Users": true, "Roles": true, "Settings": true, "Audit": true, "Media": true, "Seo": true, "Search": true,
|
||||
"Pages": true, "Menus": true, "Redirects": true, "Forms": true, "Documents": true, "Banners": true, "Faq": true, "Team": true, "Testimonials": true,
|
||||
"Categories": true, "Tags": true, "Authors": true, "Blog": true, "News": true, "Comments": true, "Feeds": true, "Sitemap": true
|
||||
"Stylist": true,
|
||||
"Core": true,
|
||||
"Administration": true,
|
||||
"Users": true,
|
||||
"Roles": true,
|
||||
"Settings": true,
|
||||
"Audit": true,
|
||||
"Media": true,
|
||||
"Seo": true,
|
||||
"Search": true,
|
||||
"Pages": true,
|
||||
"Menus": true,
|
||||
"Redirects": true,
|
||||
"Forms": true,
|
||||
"Documents": true,
|
||||
"Banners": true,
|
||||
"Faq": true,
|
||||
"Team": true,
|
||||
"Testimonials": true,
|
||||
"Categories": true,
|
||||
"Tags": true,
|
||||
"Authors": true,
|
||||
"Blog": true,
|
||||
"News": true,
|
||||
"Comments": true,
|
||||
"Feeds": true,
|
||||
"Sitemap": true,
|
||||
"CRM": true,
|
||||
"Newsletter": true,
|
||||
"Events": true,
|
||||
"Careers": true,
|
||||
"Locations": true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.header><x-stylist-theme-common::app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate /><flux:sidebar.collapse class="lg:hidden" /></flux:sidebar.header>
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.group :heading="__('Business')" class="grid">
|
||||
<flux:sidebar.item icon="building-office-2" :href="route('administration.crm.index')" :current="request()->routeIs('administration.crm.*')" wire:navigate>{{ __('CRM') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="envelope" :href="route('administration.newsletter.index')" :current="request()->routeIs('administration.newsletter.*')" wire:navigate>{{ __('Newsletter') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="calendar-days" :href="route('administration.events.index')" :current="request()->routeIs('administration.events.*')" wire:navigate>{{ __('Events') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="briefcase" :href="route('administration.careers.index')" :current="request()->routeIs('administration.careers.*')" wire:navigate>{{ __('Careers') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="map-pin" :href="route('administration.locations.index')" :current="request()->routeIs('administration.locations.*')" wire:navigate>{{ __('Locations') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Publishing')" class="grid">
|
||||
<flux:sidebar.item icon="newspaper" :href="route('administration.blog.index')" :current="request()->routeIs('administration.blog.*')" wire:navigate>{{ __('Blog') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="megaphone" :href="route('administration.news.index')" :current="request()->routeIs('administration.news.*')" wire:navigate>{{ __('News') }}</flux:sidebar.item>
|
||||
|
||||
Reference in New Issue
Block a user