the promoseven group crm
This commit is contained in:
commit
12e3a44046
88 files changed
+12923
No files matched your search
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Campaign;
|
||||
use App\Models\Guest;
|
||||
use App\Models\Venue;
|
||||
use App\Models\GuestVisit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function getKpis()
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
||||
return response()->json([
|
||||
'today_checkins' => GuestVisit::whereDate('visit_date', $today)->count(),
|
||||
'active_members' => Guest::whereHas('visits', function($q) use ($today) {
|
||||
$q->whereDate('visit_date', '>=', now()->subDays(30));
|
||||
})->count(),
|
||||
'total_guests' => Guest::count(),
|
||||
'total_visits' => GuestVisit::count(),
|
||||
'revenue_today' => GuestVisit::whereDate('visit_date', $today)->sum('spend_amount'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCharts()
|
||||
{
|
||||
$visitsByVenue = GuestVisit::select('venues.name', DB::raw('count(*) as visits'))
|
||||
->join('venues', 'guest_visits.venue_id', '=', 'venues.id')
|
||||
->groupBy('venues.id', 'venues.name')
|
||||
->get();
|
||||
|
||||
$tierDistribution = Guest::select('loyalty_tiers.name', DB::raw('count(*) as count'))
|
||||
->join('loyalty_tiers', 'guests.tier_id', '=', 'loyalty_tiers.id')
|
||||
->groupBy('loyalty_tiers.id', 'loyalty_tiers.name')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'visits_by_venue' => $visitsByVenue,
|
||||
'tier_distribution' => $tierDistribution,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createCampaign(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'segment_rules' => 'required|array',
|
||||
'offer_text' => 'required|string',
|
||||
'voucher_id' => 'required|exists:vouchers,id',
|
||||
]);
|
||||
|
||||
$campaign = Campaign::create($request->all());
|
||||
|
||||
return response()->json($campaign, 201);
|
||||
}
|
||||
|
||||
public function getVenues()
|
||||
{
|
||||
return response()->json(Venue::all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Guest;
|
||||
use App\Models\GuestVisit;
|
||||
use App\Models\GuestTag;
|
||||
use App\Models\GuestNote;
|
||||
use App\Services\GuestSearch;
|
||||
use App\Services\CrossSellEngine;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use SimpleSoftwareIO\QrCode\Facades\QrCode;
|
||||
|
||||
class GuestController extends Controller
|
||||
{
|
||||
protected $guestSearch;
|
||||
protected $crossSellEngine;
|
||||
|
||||
public function __construct(GuestSearch $guestSearch, CrossSellEngine $crossSellEngine)
|
||||
{
|
||||
$this->guestSearch = $guestSearch;
|
||||
$this->crossSellEngine = $crossSellEngine;
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$query = $request->get('q');
|
||||
if (!$query) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$guests = $this->guestSearch->search($query);
|
||||
return response()->json($guests);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$guest = Guest::with(['tier', 'visits.venue', 'tags', 'notes.staff'])->findOrFail($id);
|
||||
|
||||
return response()->json($guest);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'phone' => 'required|string|max:20',
|
||||
'venue_id' => 'required|exists:venues,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$guest = Guest::create([
|
||||
'name' => $request->name,
|
||||
'phone' => $request->phone,
|
||||
'email' => $request->email,
|
||||
'nationality' => $request->nationality,
|
||||
'language' => $request->language,
|
||||
'birthday' => $request->birthday,
|
||||
'marketing_consent' => $request->marketing_consent ?? false,
|
||||
]);
|
||||
|
||||
$guest->generateLoyaltyId();
|
||||
|
||||
// Create initial visit
|
||||
GuestVisit::create([
|
||||
'guest_id' => $guest->id,
|
||||
'venue_id' => $request->venue_id,
|
||||
'visit_date' => now(),
|
||||
'visit_type' => 'registration',
|
||||
]);
|
||||
|
||||
return response()->json($guest, 201);
|
||||
}
|
||||
|
||||
public function checkin(Request $request, $id)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'venue_id' => 'required|exists:venues,id',
|
||||
'spend_amount' => 'numeric|min:0',
|
||||
'visit_type' => 'required|in:stay,dining,bar,beach,pool,watersports',
|
||||
'room_number' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$visit = GuestVisit::create([
|
||||
'guest_id' => $id,
|
||||
'venue_id' => $request->venue_id,
|
||||
'visit_date' => now(),
|
||||
'spend_amount' => $request->spend_amount ?? 0,
|
||||
'visit_type' => $request->visit_type,
|
||||
'room_number' => $request->room_number,
|
||||
'notes' => $request->notes,
|
||||
]);
|
||||
|
||||
// Update lifetime spend
|
||||
$guest = Guest::find($id);
|
||||
$guest->lifetime_spend = $guest->visits()->sum('spend_amount');
|
||||
$guest->save();
|
||||
|
||||
return response()->json($visit, 201);
|
||||
}
|
||||
|
||||
public function getCrossSellSuggestions($id)
|
||||
{
|
||||
$suggestions = $this->crossSellEngine->getSuggestions($id);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
public function addTags(Request $request, $id)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
foreach ($request->tags as $tagName) {
|
||||
GuestTag::firstOrCreate([
|
||||
'guest_id' => $id,
|
||||
'tag_name' => $tagName,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Tags added successfully']);
|
||||
}
|
||||
|
||||
public function addNote(Request $request, $id)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'note_text' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$note = GuestNote::create([
|
||||
'guest_id' => $id,
|
||||
'note_text' => $request->note_text,
|
||||
'staff_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json($note, 201);
|
||||
}
|
||||
|
||||
public function generateQrCode($id)
|
||||
{
|
||||
$guest = Guest::findOrFail($id);
|
||||
$qrCode = QrCode::size(200)->generate($guest->loyalty_id);
|
||||
|
||||
return response($qrCode)->header('Content-Type', 'image/svg+xml');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Venue;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$venues = Venue::all();
|
||||
return view('dashboard', compact('venues'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GuestWebController extends Controller
|
||||
{
|
||||
public function search()
|
||||
{
|
||||
return view('guests.search');
|
||||
}
|
||||
|
||||
public function new()
|
||||
{
|
||||
return view('guests.new');
|
||||
}
|
||||
|
||||
public function profile($id)
|
||||
{
|
||||
return view('guests.profile', ['guestId' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Campaign extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'segment_rules',
|
||||
'offer_text',
|
||||
'voucher_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'segment_rules' => 'array',
|
||||
];
|
||||
|
||||
public function voucher()
|
||||
{
|
||||
return $this->belongsTo(Voucher::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Guest extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GuestNote extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'guest_id',
|
||||
'note_text',
|
||||
'staff_id',
|
||||
];
|
||||
|
||||
public function guest()
|
||||
{
|
||||
return $this->belongsTo(Guest::class);
|
||||
}
|
||||
|
||||
public function staff()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'staff_id');
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GuestVisit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'guest_id',
|
||||
'venue_id',
|
||||
'visit_date',
|
||||
'spend_amount',
|
||||
'visit_type',
|
||||
'room_number',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'visit_date' => 'datetime',
|
||||
'spend_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function guest()
|
||||
{
|
||||
return $this->belongsTo(Guest::class);
|
||||
}
|
||||
|
||||
public function venue()
|
||||
{
|
||||
return $this->belongsTo(Venue::class);
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Voucher extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'discount_amount',
|
||||
'valid_venues',
|
||||
'expiry_date',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'valid_venues' => 'array',
|
||||
'expiry_date' => 'datetime',
|
||||
'discount_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function issuedVouchers()
|
||||
{
|
||||
return $this->hasMany(IssuedVoucher::class);
|
||||
}
|
||||
|
||||
public function campaigns()
|
||||
{
|
||||
return $this->hasMany(Campaign::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Guest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GuestSearch
|
||||
{
|
||||
public function search(string $query)
|
||||
{
|
||||
return Guest::where('name', 'like', "%{$query}%")
|
||||
->orWhere('phone', 'like', "%{$query}%")
|
||||
->orWhere('email', 'like', "%{$query}%")
|
||||
->orderByRaw("CASE WHEN phone LIKE '%{$query}%' THEN 1 ELSE 2 END")
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(20)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user