Merge pull request #8 from okorpheus/invoicing

Invoicing Feature Complete
This commit is contained in:
Matt 2024-06-29 10:27:15 -05:00 committed by GitHub
commit 247656f60b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 494 additions and 146 deletions

View File

@ -21,9 +21,15 @@ class AuditionSettings extends Controller
'organizerName' => ['required'], 'organizerName' => ['required'],
'organizerEmail' => ['required', 'email'], 'organizerEmail' => ['required', 'email'],
'registrationCode' => ['required'], 'registrationCode' => ['required'],
'late_fee' => ['nullable', 'numeric'], 'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent'], // Options should align with the boot method of InvoiceDataServiceProvider
'school_fee' => ['nullable', 'numeric'], 'late_fee' => ['nullable', 'numeric', 'min:0'],
'school_fee' => ['nullable', 'numeric', 'min:0'],
]); ]);
// Store currency values as cents
$validData['late_fee'] = $validData['late_fee'] * 100;
$validData['school_fee'] = $validData['school_fee'] * 100;
// TODO implement olympic scoring // TODO implement olympic scoring
foreach ($validData as $key => $value) { foreach ($validData as $key => $value) {
Settings::set($key, $value); Settings::set($key, $value);

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\School; use App\Models\School;
use App\Models\SchoolEmailDomain; use App\Models\SchoolEmailDomain;
use App\Services\Invoice\InvoiceDataService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -14,14 +15,25 @@ use function request;
class SchoolController extends Controller class SchoolController extends Controller
{ {
protected $invoiceService;
public function __construct(InvoiceDataService $invoiceController)
{
$this->invoiceService = $invoiceController;
}
public function index() public function index()
{ {
if (! Auth::user()->is_admin) { if (! Auth::user()->is_admin) {
abort(403); abort(403);
} }
$schools = School::with(['users', 'students', 'entries'])->orderBy('name')->get(); $schools = School::with(['users', 'students', 'entries'])->orderBy('name')->get();
$schoolTotalFees = [];
foreach ($schools as $school) {
$schoolTotalFees[$school->id] = $this->invoiceService->getGrandTotal($school->id);
}
return view('admin.schools.index', ['schools' => $schools]); return view('admin.schools.index', compact('schools', 'schoolTotalFees'));
} }
public function show(Request $request, School $school) public function show(Request $request, School $school)
@ -122,4 +134,11 @@ class SchoolController extends Controller
// return a redirect to the previous URL // return a redirect to the previous URL
return redirect()->back(); return redirect()->back();
} }
public function viewInvoice(Request $request, School $school)
{
$invoiceData = $this->invoiceService->allData($school->id);
return view('dashboard.invoice', compact('school', 'invoiceData'));
}
} }

View File

@ -2,13 +2,20 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use function dd;
use function redirect; use function redirect;
class DashboardController extends Controller class DashboardController extends Controller
{ {
protected InvoiceDataService $invoiceService;
public function __construct(InvoiceDataService $invoiceService)
{
$this->invoiceService = $invoiceService;
}
public function profile() public function profile()
{ {
return view('dashboard.profile'); return view('dashboard.profile');
@ -22,10 +29,23 @@ class DashboardController extends Controller
public function my_school() public function my_school()
{ {
if (Auth::user()->school) { if (Auth::user()->school) {
return redirect('/schools/' . Auth::user()->school->id); return redirect('/schools/'.Auth::user()->school->id);
} }
$possibilities = Auth::user()->possibleSchools(); $possibilities = Auth::user()->possibleSchools();
if (count($possibilities) < 1) return view('schools.create'); if (count($possibilities) < 1) {
return view('schools.create');
}
return view('dashboard.select_school', ['possibilities' => $possibilities]); return view('dashboard.select_school', ['possibilities' => $possibilities]);
} }
public function my_invoice()
{
if (! Auth::user()->school_id) {
return redirect()->route('dashboard')->with('error', 'You do not have a school to get an invoice for');
}
$invoiceData = $this->invoiceService->allData(Auth::user()->school_id);
$school = Auth::user()->school;
return view('dashboard.invoice', compact('school', 'invoiceData'));
}
} }

View File

@ -4,20 +4,20 @@ namespace App\Http\Controllers;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Seat; use App\Models\Seat;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use App\Services\SeatingService; use App\Services\SeatingService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class ResultsPage extends Controller class ResultsPage extends Controller
{ {
protected $auditionCacheService; protected $auditionService;
protected $seatingService; protected $seatingService;
public function __construct(AuditionCacheService $auditionCacheService, SeatingService $seatingService) public function __construct(AuditionService $auditionService, SeatingService $seatingService)
{ {
$this->auditionCacheService = $auditionCacheService; $this->auditionService = $auditionService;
$this->seatingService = $seatingService; $this->seatingService = $seatingService;
} }
@ -26,7 +26,7 @@ class ResultsPage extends Controller
*/ */
public function __invoke(Request $request) public function __invoke(Request $request)
{ {
$publishedAuditions = $this->auditionCacheService->getPublishedAuditions(); $publishedAuditions = $this->auditionService->getPublishedAuditions();
$resultsSeatList = Cache::rememberForever('resultsSeatList', function () use ($publishedAuditions) { $resultsSeatList = Cache::rememberForever('resultsSeatList', function () use ($publishedAuditions) {
$seatList = []; $seatList = [];
// Load the $seatList in the form of $seatlist[audition_id] is an array of seats for that audition // Load the $seatList in the form of $seatlist[audition_id] is an array of seats for that audition
@ -51,7 +51,7 @@ class ResultsPage extends Controller
return $seatList; return $seatList;
}); });
$publishedAdvancementAuditions = $this->auditionCacheService->getPublishedAdvancementAuditions(); $publishedAdvancementAuditions = $this->auditionService->getPublishedAdvancementAuditions();
$resultsAdvancementList = Cache::rememberForever('resultsAdvancementList', function () use ($publishedAdvancementAuditions) { $resultsAdvancementList = Cache::rememberForever('resultsAdvancementList', function () use ($publishedAdvancementAuditions) {
$qualifierList = []; $qualifierList = [];
foreach ($publishedAdvancementAuditions as $audition) { foreach ($publishedAdvancementAuditions as $audition) {

View File

@ -6,14 +6,14 @@ use App\Http\Controllers\Controller;
use App\Models\Entry; use App\Models\Entry;
use App\Models\EntryFlag; use App\Models\EntryFlag;
use App\Services\DoublerService; use App\Services\DoublerService;
use App\Services\EntryCacheService; use App\Services\EntryService;
class DoublerDecisionController extends Controller class DoublerDecisionController extends Controller
{ {
protected $doublerService; protected $doublerService;
protected $entryService; protected $entryService;
public function __construct(DoublerService $doublerService, EntryCacheService $entryService) public function __construct(DoublerService $doublerService, EntryService $entryService)
{ {
$this->doublerService = $doublerService; $this->doublerService = $doublerService;
$this->entryService = $entryService; $this->entryService = $entryService;

View File

@ -5,7 +5,7 @@ namespace App\Http\Controllers\Tabulation;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Seat; use App\Models\Seat;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use App\Services\DoublerService; use App\Services\DoublerService;
use App\Services\SeatingService; use App\Services\SeatingService;
use App\Services\TabulationService; use App\Services\TabulationService;
@ -22,17 +22,17 @@ class TabulationController extends Controller
protected $seatingService; protected $seatingService;
protected $auditionCacheService; protected $auditionService;
public function __construct(TabulationService $tabulationService, public function __construct(TabulationService $tabulationService,
DoublerService $doublerService, DoublerService $doublerService,
SeatingService $seatingService, SeatingService $seatingService,
AuditionCacheService $auditionCacheService) AuditionService $auditionService)
{ {
$this->tabulationService = $tabulationService; $this->tabulationService = $tabulationService;
$this->doublerService = $doublerService; $this->doublerService = $doublerService;
$this->seatingService = $seatingService; $this->seatingService = $seatingService;
$this->auditionCacheService = $auditionCacheService; $this->auditionService = $auditionService;
} }
public function status() public function status()

View File

@ -2,7 +2,8 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use App\Services\Invoice\InvoiceDataService;
use App\Services\TabulationService; use App\Services\TabulationService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
@ -11,16 +12,22 @@ class TestController extends Controller
{ {
protected $scoringGuideCacheService; protected $scoringGuideCacheService;
protected $tabulationService; protected $tabulationService;
protected $invoiceService;
public function __construct(AuditionCacheService $scoringGuideCacheService, TabulationService $tabulationService) public function __construct(
{ AuditionService $scoringGuideCacheService,
TabulationService $tabulationService,
InvoiceDataService $invoiceService
) {
$this->scoringGuideCacheService = $scoringGuideCacheService; $this->scoringGuideCacheService = $scoringGuideCacheService;
$this->tabulationService = $tabulationService; $this->tabulationService = $tabulationService;
$this->invoiceService = $invoiceService;
} }
public function flashTest(Request $request) public function flashTest(Request $request)
{ {
$auditions = $this->tabulationService->getAuditionsWithStatus(); $lines = $this->invoiceService->getLines(12);
return view('test', compact('auditions')); $totalFees = $this->invoiceService->getGrandTotal(12);
return view('test', compact('lines','totalFees'));
} }
} }

View File

@ -3,19 +3,18 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\AuditionChange; use App\Events\AuditionChange;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class RefreshAuditionCache class RefreshAuditionCache
{ {
protected $auditionCacheService; protected $auditionService;
/** /**
* Create the event listener. * Create the event listener.
*/ */
public function __construct(AuditionCacheService $cacheService) public function __construct(AuditionService $cacheService)
{ {
$this->auditionCacheService = $cacheService; $this->auditionService = $cacheService;
} }
/** /**
@ -24,9 +23,9 @@ class RefreshAuditionCache
public function handle(AuditionChange $event): void public function handle(AuditionChange $event): void
{ {
if ($event->refreshCache) { if ($event->refreshCache) {
$this->auditionCacheService->refreshCache(); $this->auditionService->refreshCache();
} else { } else {
$this->auditionCacheService->clearCache(); $this->auditionService->clearCache();
} }
} }
} }

View File

@ -4,20 +4,20 @@ namespace App\Listeners;
use App\Events\AuditionChange; use App\Events\AuditionChange;
use App\Events\EntryChange; use App\Events\EntryChange;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use App\Services\EntryCacheService; use App\Services\EntryService;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
class RefreshEntryCache class RefreshEntryCache
{ {
protected $entryCacheService; protected $entryService;
/** /**
* Create the event listener. * Create the event listener.
*/ */
public function __construct(EntryCacheService $cacheService) public function __construct(EntryService $cacheService)
{ {
$this->entryCacheService = $cacheService; $this->entryService = $cacheService;
} }
/** /**
@ -26,9 +26,9 @@ class RefreshEntryCache
public function handle(EntryChange $event): void public function handle(EntryChange $event): void
{ {
if ($event->auditionId) { if ($event->auditionId) {
$this->entryCacheService->clearEntryCacheForAudition($event->auditionId); $this->entryService->clearEntryCacheForAudition($event->auditionId);
} else { } else {
$this->entryCacheService->clearEntryCaches(); $this->entryService->clearEntryCaches();
} }
} }
} }

View File

@ -17,25 +17,28 @@ class School extends Model
{ {
return $this->hasMany(User::class); return $this->hasMany(User::class);
} }
public function users(): HasMany public function users(): HasMany
{ {
return $this->hasMany(User::class); return $this->hasMany(User::class);
} }
public function emailDomains(): HasMany public function emailDomains(): HasMany
{ {
return $this->hasMany(SchoolEmailDomain::class); return $this->hasMany(SchoolEmailDomain::class);
} }
public function initialLetterImageURL($bg_color = '4f46e5', $text_color='fff'): string public function initialLetterImageURL($bg_color = '4f46e5', $text_color = 'fff'): string
{ {
$img = "https://ui-avatars.com/api/?background=$bg_color&color=$text_color&name="; $img = "https://ui-avatars.com/api/?background=$bg_color&color=$text_color&name=";
$img .= substr($this->name,0,1); $img .= substr($this->name, 0, 1);
return $img; return $img;
} }
public function students(): HasMany public function students(): HasMany
{ {
return $this->hasMany(Student::class); return $this->hasMany(Student::class)->orderBy('last_name')->orderBy('first_name');
} }
public function entries(): HasManyThrough public function entries(): HasManyThrough
@ -48,5 +51,4 @@ class School extends Model
'id', 'id',
'id'); 'id');
} }
} }

View File

@ -34,9 +34,9 @@ use App\Observers\SeatingLimitObserver;
use App\Observers\StudentObserver; use App\Observers\StudentObserver;
use App\Observers\SubscoreDefinitionObserver; use App\Observers\SubscoreDefinitionObserver;
use App\Observers\UserObserver; use App\Observers\UserObserver;
use App\Services\AuditionCacheService; use App\Services\AuditionService;
use App\Services\DoublerService; use App\Services\DoublerService;
use App\Services\EntryCacheService; use App\Services\EntryService;
use App\Services\ScoreService; use App\Services\ScoreService;
use App\Services\SeatingService; use App\Services\SeatingService;
use App\Services\TabulationService; use App\Services\TabulationService;
@ -50,31 +50,31 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
$this->app->singleton(AuditionCacheService::class, function () { $this->app->singleton(AuditionService::class, function () {
return new AuditionCacheService(); return new AuditionService();
}); });
$this->app->singleton(SeatingService::class, function ($app) { $this->app->singleton(SeatingService::class, function ($app) {
return new SeatingService($app->make(TabulationService::class)); return new SeatingService($app->make(TabulationService::class));
}); });
$this->app->singleton(EntryCacheService::class, function ($app) { $this->app->singleton(EntryService::class, function ($app) {
return new EntryCacheService($app->make(AuditionCacheService::class)); return new EntryService($app->make(AuditionService::class));
}); });
$this->app->singleton(ScoreService::class, function ($app) { $this->app->singleton(ScoreService::class, function ($app) {
return new ScoreService($app->make(AuditionCacheService::class), $app->make(EntryCacheService::class)); return new ScoreService($app->make(AuditionService::class), $app->make(EntryService::class));
}); });
$this->app->singleton(TabulationService::class, function ($app) { $this->app->singleton(TabulationService::class, function ($app) {
return new TabulationService( return new TabulationService(
$app->make(AuditionCacheService::class), $app->make(AuditionService::class),
$app->make(ScoreService::class), $app->make(ScoreService::class),
$app->make(EntryCacheService::class)); $app->make(EntryService::class));
}); });
$this->app->singleton(DoublerService::class, function ($app) { $this->app->singleton(DoublerService::class, function ($app) {
return new DoublerService($app->make(AuditionCacheService::class), $app->make(TabulationService::class), $app->make(SeatingService::class)); return new DoublerService($app->make(AuditionService::class), $app->make(TabulationService::class), $app->make(SeatingService::class));
}); });
} }

View File

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use App\Services\EntryService;
use App\Services\Invoice\InvoiceDataService;
use App\Services\Invoice\InvoiceOneFeePerEntry;
use App\Services\Invoice\InvoiceOneFeePerStudent;
use Illuminate\Support\ServiceProvider;
use function auditionSetting;
class InvoiceDataServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(InvoiceDataService::class, function ($app) {
// Default binding, can be overridden in booth method
return new InvoiceOneFeePerEntry($app->make(EntryService::class));
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->app->singleton(InvoiceDataService::class, function ($app) {
return match (auditionSetting('fee_structure')) {
'oneFeePerEntry' => new InvoiceOneFeePerEntry($app->make(EntryService::class)),
'oneFeePerStudent' => new InvoiceOneFeePerStudent($app->make(EntryService::class)),
default => throw new \Exception('Unknown Invoice Method'),
};
});
}
}

View File

@ -9,7 +9,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
class AuditionCacheService class AuditionService
{ {
protected $cacheKey = 'auditions'; protected $cacheKey = 'auditions';

View File

@ -11,7 +11,7 @@ class DoublerService
{ {
protected $doublersCacheKey = 'doublers'; protected $doublersCacheKey = 'doublers';
protected $auditionCacheService; protected $auditionService;
protected $tabulationService; protected $tabulationService;
@ -20,9 +20,9 @@ class DoublerService
/** /**
* Create a new class instance. * Create a new class instance.
*/ */
public function __construct(AuditionCacheService $auditionCacheService, TabulationService $tabulationService, SeatingService $seatingService) public function __construct(AuditionService $auditionService, TabulationService $tabulationService, SeatingService $seatingService)
{ {
$this->auditionCacheService = $auditionCacheService; $this->auditionService = $auditionService;
$this->tabulationService = $tabulationService; $this->tabulationService = $tabulationService;
$this->seatingService = $seatingService; $this->seatingService = $seatingService;
} }
@ -100,13 +100,13 @@ class DoublerService
$info[$entry->id] = [ $info[$entry->id] = [
'entryID' => $entry->id, 'entryID' => $entry->id,
'auditionID' => $entry->audition_id, 'auditionID' => $entry->audition_id,
'auditionName' => $this->auditionCacheService->getAudition($entry->audition_id)->name, 'auditionName' => $this->auditionService->getAudition($entry->audition_id)->name,
'rank' => $this->tabulationService->entryRank($entry), 'rank' => $this->tabulationService->entryRank($entry),
'unscored' => $this->tabulationService->remainingEntriesForAudition($entry->audition_id), 'unscored' => $this->tabulationService->remainingEntriesForAudition($entry->audition_id),
'limits' => $this->seatingService->getLimitForAudition($entry->audition_id), 'limits' => $this->seatingService->getLimitForAudition($entry->audition_id),
'status' => $status, 'status' => $status,
]; ];
$entry->audition = $this->auditionCacheService->getAudition($entry->audition_id); $entry->audition = $this->auditionService->getAudition($entry->audition_id);
} }
return $info; return $info;

View File

@ -6,14 +6,14 @@ use App\Models\Entry;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class EntryCacheService class EntryService
{ {
protected $auditionCache; protected $auditionCache;
/** /**
* Create a new class instance. * Create a new class instance.
*/ */
public function __construct(AuditionCacheService $auditionCache) public function __construct(AuditionService $auditionCache)
{ {
$this->auditionCache = $auditionCache; $this->auditionCache = $auditionCache;
} }
@ -89,4 +89,13 @@ class EntryCacheService
$this->clearEntryCacheForAudition($audition->id); $this->clearEntryCacheForAudition($audition->id);
} }
} }
public function entryIsLate(Entry $entry): bool
{
if ($entry->hasFlag('wave_late_fee')) {
return false;
}
return $entry->created_at > $entry->audition->entry_deadline;
}
} }

View File

@ -0,0 +1,18 @@
<?php
namespace App\Services\Invoice;
interface InvoiceDataService
{
public function allData($schoolId);
public function getLines($schoolId);
public function getLinesTotal($schoolId);
public function getLateFeesTotal($schoolId);
public function getSchoolFeeTotal($schoolId);
public function getGrandTotal($schoolId);
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Services\Invoice;
use App\Models\School;
use App\Services\EntryService;
use Illuminate\Support\Arr;
use function auditionSetting;
class InvoiceOneFeePerEntry implements InvoiceDataService
{
/**
* Create a new class instance.
*/
protected $entryService;
public function __construct(EntryService $entryService)
{
$this->entryService = $entryService;
}
public function allData($schoolId)
{
static $schoolInvoiceData = [];
if (Arr::has($schoolInvoiceData, $schoolId)) {
return $schoolInvoiceData[$schoolId];
}
$school = School::findOrFail($schoolId);
$invoiceData['lines'] = [];
$invoiceData['linesTotal'] = 0;
$invoiceData['lateFeesTotal'] = 0;
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
foreach ($entries[$student->id] ?? [] as $entry) {
$entryFee = $entry->audition->entry_fee / 100;
$lateFee = $this->entryService->entryIsLate($entry) ? auditionSetting('late_fee') / 100 : 0;
$invoiceData['lines'][] = [
'student_name' => $student->full_name(true),
'audition' => $entry->audition->name,
'entry_timestamp' => $entry->created_at,
'entry_fee' => $entryFee,
'late_fee' => $lateFee,
];
$invoiceData['linesTotal'] += $entryFee;
$invoiceData['lateFeesTotal'] += $lateFee;
}
}
// School Fee Total
if (! auditionSetting('school_fee')) {
$invoiceData['schoolFeeTotal'] = 0;
} else {
$invoiceData['schoolFeeTotal'] = auditionSetting('school_fee') / 100;
}
$invoiceData['grandTotal'] = $invoiceData['linesTotal'] + $invoiceData['lateFeesTotal'] + $invoiceData['schoolFeeTotal'];
$schoolInvoiceData[$school->id] = $invoiceData;
return $invoiceData;
}
public function getLines($schoolId)
{
return $this->allData($schoolId)['lines'];
}
public function getLinesTotal($schoolId)
{
return $this->allData($schoolId)['linesTotal'];
}
public function getLateFeesTotal($schoolId)
{
return $this->allData($schoolId)['lateFeesTotal'];
}
public function getSchoolFeeTotal($schoolId)
{
return $this->allData($schoolId)['schoolFeeTotal'];
}
public function getGrandTotal($schoolId)
{
return $this->allData($schoolId)['grandTotal'];
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Services\Invoice;
use App\Models\School;
use App\Services\EntryService;
use Illuminate\Support\Arr;
use function auditionSetting;
class InvoiceOneFeePerStudent implements InvoiceDataService
{
/**
* Create a new class instance.
*/
protected $entryService;
public function __construct(EntryService $entryService)
{
$this->entryService = $entryService;
}
public function allData($schoolId)
{
static $schoolInvoiceData = [];
if (Arr::has($schoolInvoiceData, $schoolId)) {
return $schoolInvoiceData[$schoolId];
}
$school = School::findOrFail($schoolId);
$invoiceData['lines'] = [];
$invoiceData['linesTotal'] = 0;
$invoiceData['lateFeesTotal'] = 0;
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
$firstEntryForStudent = true;
foreach ($entries[$student->id] ?? [] as $entry) {
if ($firstEntryForStudent) {
$entryFee = $entry->audition->entry_fee / 100;
$lateFee = $this->entryService->entryIsLate($entry) ? auditionSetting('late_fee') / 100 : 0;
} else {
$entryFee = 0;
$lateFee = 0;
}
$invoiceData['lines'][] = [
'student_name' => $student->full_name(true),
'audition' => $entry->audition->name,
'entry_timestamp' => $entry->created_at,
'entry_fee' => $entryFee,
'late_fee' => $lateFee,
];
$invoiceData['linesTotal'] += $entryFee;
$invoiceData['lateFeesTotal'] += $lateFee;
$firstEntryForStudent = false;
}
}
// School Fee Total
if (! auditionSetting('school_fee')) {
$invoiceData['schoolFeeTotal'] = 0;
} else {
$invoiceData['schoolFeeTotal'] = auditionSetting('school_fee') / 100;
}
$invoiceData['grandTotal'] = $invoiceData['linesTotal'] + $invoiceData['lateFeesTotal'] + $invoiceData['schoolFeeTotal'];
$schoolInvoiceData[$school->id] = $invoiceData;
return $invoiceData;
}
public function getLines($schoolId)
{
return $this->allData($schoolId)['lines'];
}
public function getLinesTotal($schoolId)
{
return $this->allData($schoolId)['linesTotal'];
}
public function getLateFeesTotal($schoolId)
{
return $this->allData($schoolId)['lateFeesTotal'];
}
public function getSchoolFeeTotal($schoolId)
{
return $this->allData($schoolId)['schoolFeeTotal'];
}
public function getGrandTotal($schoolId)
{
return $this->allData($schoolId)['grandTotal'];
}
}

View File

@ -20,7 +20,7 @@ class ScoreService
/** /**
* Create a new class instance. * Create a new class instance.
*/ */
public function __construct(AuditionCacheService $auditionCache, EntryCacheService $entryCache) public function __construct(AuditionService $auditionCache, EntryService $entryCache)
{ {
$this->auditionCache = $auditionCache; $this->auditionCache = $auditionCache;
$this->entryCache = $entryCache; $this->entryCache = $entryCache;

View File

@ -9,9 +9,9 @@ use Illuminate\Support\Facades\Session;
class TabulationService class TabulationService
{ {
protected AuditionCacheService $auditionCacheService; protected AuditionService $auditionService;
protected EntryCacheService $entryCacheService; protected EntryService $entryService;
protected ScoreService $scoreService; protected ScoreService $scoreService;
@ -19,13 +19,13 @@ class TabulationService
* Create a new class instance. * Create a new class instance.
*/ */
public function __construct( public function __construct(
AuditionCacheService $scoringGuideCacheService, AuditionService $auditionService,
ScoreService $scoreService, ScoreService $scoreService,
EntryCacheService $entryCacheService) EntryService $entryService)
{ {
$this->auditionCacheService = $scoringGuideCacheService; $this->auditionService = $auditionService;
$this->scoreService = $scoreService; $this->scoreService = $scoreService;
$this->entryCacheService = $entryCacheService; $this->entryService = $entryService;
} }
/** /**
@ -51,8 +51,8 @@ class TabulationService
return $cache[$auditionId]; return $cache[$auditionId];
} }
$audition = $this->auditionCacheService->getAudition($auditionId); $audition = $this->auditionService->getAudition($auditionId);
$entries = $this->entryCacheService->getEntriesForAudition($auditionId, $mode); $entries = $this->entryService->getEntriesForAudition($auditionId, $mode);
$this->scoreService->calculateScoresForAudition($auditionId); $this->scoreService->calculateScoresForAudition($auditionId);
// TODO will need to pass a mode to the above function to only use subscores for hte appropriate mode // TODO will need to pass a mode to the above function to only use subscores for hte appropriate mode
foreach ($entries as $entry) { foreach ($entries as $entry) {
@ -91,7 +91,7 @@ class TabulationService
public function entryScoreSheetsAreValid(Entry $entry): bool public function entryScoreSheetsAreValid(Entry $entry): bool
{ {
//TODO consider making this move the invalid score to another database for further investigation //TODO consider making this move the invalid score to another database for further investigation
$validJudges = $this->auditionCacheService->getAudition($entry->audition_id)->judges; $validJudges = $this->auditionService->getAudition($entry->audition_id)->judges;
foreach ($entry->scoreSheets as $sheet) { foreach ($entry->scoreSheets as $sheet) {
if (! $validJudges->contains($sheet->user_id)) { if (! $validJudges->contains($sheet->user_id)) {
$invalidJudge = User::find($sheet->user_id); $invalidJudge = User::find($sheet->user_id);
@ -135,11 +135,11 @@ class TabulationService
return Cache::remember('auditionsWithStatus', 30, function () use ($mode) { return Cache::remember('auditionsWithStatus', 30, function () use ($mode) {
// Retrieve auditions from the cache and load entry IDs // Retrieve auditions from the cache and load entry IDs
$auditions = $this->auditionCacheService->getAuditions($mode); $auditions = $this->auditionService->getAuditions($mode);
// Iterate over the auditions and calculate the scored_entries_count // Iterate over the auditions and calculate the scored_entries_count
foreach ($auditions as $audition) { foreach ($auditions as $audition) {
$scored_entries_count = 0; $scored_entries_count = 0;
$entries_to_check = $this->entryCacheService->getEntriesForAudition($audition->id); $entries_to_check = $this->entryService->getEntriesForAudition($audition->id);
switch ($mode) { switch ($mode) {
case 'seating': case 'seating':

View File

@ -3,4 +3,5 @@
return [ return [
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class, App\Providers\FortifyServiceProvider::class,
App\Providers\InvoiceDataServiceProvider::class,
]; ];

View File

@ -24,7 +24,6 @@
<x-layout.page-section> <x-layout.page-section>
<x-slot:section_name>Scoring Settings</x-slot:section_name> <x-slot:section_name>Scoring Settings</x-slot:section_name>
<x-slot:section_description>If students cannot advance to further honor groups, leave next event name blank</x-slot:section_description>
<x-form.body-grid columns="12" class="m-3"> <x-form.body-grid columns="12" class="m-3">
<div class="col-span-6 flex space-x-3"> <div class="col-span-6 flex space-x-3">
<x-form.toggle-checkbox name="judging_enabled"/><span>Enable score entry by judges</span> <x-form.toggle-checkbox name="judging_enabled"/><span>Enable score entry by judges</span>
@ -37,17 +36,27 @@
<x-layout.page-section> <x-layout.page-section>
<x-slot:section_name>Financial Settings</x-slot:section_name> <x-slot:section_name>Financial Settings</x-slot:section_name>
<x-slot:section_description>If students cannot advance to further honor groups, leave next event name blank</x-slot:section_description>
<x-form.body-grid columns="12" class="m-3"> <x-form.body-grid columns="12" class="m-3">
<x-form.select name="fee_structure" colspan="6"> <x-form.select name="fee_structure" colspan="6">
<x-slot:label>Fee Structure</x-slot:label> <x-slot:label>Fee Structure</x-slot:label>
<option>One fee per entry</option> {{-- Values should be one of the options in the boot method InvoiceDataServiceProvider --}}
<option>One fee per student</option> <option value="oneFeePerEntry" {{ auditionSetting('fee_structure') === 'oneFeePerEntry' ? 'selected':'' }}>
One fee per entry
</option>
<option value="oneFeePerStudent" {{ auditionSetting('fee_structure') === 'oneFeePerStudent' ? 'selected':'' }}>
One fee per student - one late fee per student if any of their entries are late
</option>
</x-form.select> </x-form.select>
<x-form.field label_text="Late Fee" name="late_fee" colspan="3" :value="auditionSetting('late_fee')"/> <x-form.field label_text="Late Fee"
<x-form.field label_text="School Membership Fee" name="school_fee" colspan="3" :value="auditionSetting('school_fee')"/> name="late_fee"
colspan="3"
:value="number_format(auditionSetting('late_fee') / 100,2) "/>
<x-form.field label_text="School Membership Fee"
name="school_fee"
colspan="3"
:value="number_format(auditionSetting('school_fee') / 100,2)"/>
</x-form.body-grid> </x-form.body-grid>
</x-layout.page-section> </x-layout.page-section>

View File

@ -4,7 +4,7 @@
<x-card.card> <x-card.card>
<x-table.table with_title_area> <x-table.table with_title_area>
<x-slot:title class="ml-3">Schools</x-slot:title> <x-slot:title class="ml-3">Schools</x-slot:title>
<x-slot:subtitle class="ml-3">Click school name to edit</x-slot:subtitle> <x-slot:subtitle class="ml-3">Click school name to edit<br>Click total fees for invoice</x-slot:subtitle>
<x-slot:title_block_right class="mr-3"> <x-slot:title_block_right class="mr-3">
<x-form.button href="{{ route('admin.schools.create') }}">New School</x-form.button> <x-form.button href="{{ route('admin.schools.create') }}">New School</x-form.button>
</x-slot:title_block_right> </x-slot:title_block_right>
@ -12,6 +12,7 @@
<thead> <thead>
<tr> <tr>
<x-table.th>Name</x-table.th> <x-table.th>Name</x-table.th>
<x-table.th>Total Fees</x-table.th>
<x-table.th>Directors</x-table.th> <x-table.th>Directors</x-table.th>
<x-table.th>Students</x-table.th> <x-table.th>Students</x-table.th>
<x-table.th>Entries</x-table.th> <x-table.th>Entries</x-table.th>
@ -22,6 +23,11 @@
@foreach($schools as $school) @foreach($schools as $school)
<tr> <tr>
<x-table.td><a href="/admin/schools/{{ $school->id }}">{{ $school->name }}</a></x-table.td> <x-table.td><a href="/admin/schools/{{ $school->id }}">{{ $school->name }}</a></x-table.td>
<x-table.td>
<a href="{{ route('admin.schools.invoice',$school->id) }}">
${{ number_format($schoolTotalFees[$school->id],2) }}
</a>
</x-table.td>
<x-table.td>{{ $school->users->count() }}</x-table.td> <x-table.td>{{ $school->users->count() }}</x-table.td>
<x-table.td>{{ $school->students->count() }}</x-table.td> <x-table.td>{{ $school->students->count() }}</x-table.td>
<x-table.td>{{ $school->entries->count() }}</x-table.td> <x-table.td>{{ $school->entries->count() }}</x-table.td>

View File

@ -1,5 +1,5 @@
@props(['right_link_button_type' => 'a']) {{-- Use if the link to the right needs to be a button --}} @props(['right_link_button_type' => 'a']) {{-- Use if the link to the right needs to be a button --}}
<li {{ $attributes->merge(['class'=>'flex items-center justify-between gap-x-6 px-4 py-5 sm:px-6']) }}> <li {{ $attributes->merge(['class'=>'flex items-center justify-between gap-x-6 px-4 py-4 sm:px-6']) }}>
<div class="flex min-w-0 gap-x-4"> <div class="flex min-w-0 gap-x-4">
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -4,4 +4,31 @@
@if(! Auth::user()->school_id) @if(! Auth::user()->school_id)
You aren't currently associated with a school. <a href="/my_school" class="text-blue-600">Click here to choose or create one.</a> You aren't currently associated with a school. <a href="/my_school" class="text-blue-600">Click here to choose or create one.</a>
@endif @endif
<div class="grid sm:grid-cols-2 md:grid-cols-4">
<div>{{-- Column 1 --}}
<x-card.card>
<x-card.heading>User Options</x-card.heading>
<x-card.list.body>
<a href="{{ route('my_profile') }}">
<x-card.list.row class="hover:bg-gray-200">
My Profile
</x-card.list.row>
</a>
<a href="{{ route('my_school') }}">
<x-card.list.row class="hover:bg-gray-200">
My School
</x-card.list.row>
</a>
@if(Auth::user()->school_id)
<a href="{{ route('my_invoice') }}">
<x-card.list.row class="hover:bg-gray-200">
My Invoice
</x-card.list.row>
</a>
@endif
</x-card.list.body>
</x-card.card>
</div>
</div>
</x-layout.app> </x-layout.app>

View File

@ -0,0 +1,50 @@
@props(['school', 'invoiceData'])
<x-layout.app>
<x-slot:page_title>Invoice - {{ $school->name }}</x-slot:page_title>
<div class="">
<x-table.table class="">
<thead class="">
<tr>
<x-table.th>Student Name</x-table.th>
<x-table.th>Audition</x-table.th>
<x-table.th>Entry Timestamp</x-table.th>
<x-table.th>Entry Fee</x-table.th>
<x-table.th>Late Fee</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($invoiceData['lines'] as $line)
<tr>
<x-table.td>{{ $line['student_name'] }}</x-table.td>
<x-table.td>{{ $line['audition'] }}</x-table.td>
<x-table.td>{{ $line['entry_timestamp']->setTimezone('America/Chicago')->format('m/d/Y g:i:s A') }}</x-table.td>
<x-table.td>${{ number_format($line['entry_fee'],2) }}</x-table.td>
<x-table.td>${{ number_format($line['late_fee'],2) }}</x-table.td>
</tr>
@endforeach
</x-table.body>
<tfoot class="">
<tr>
<td colspan="2">
<x-table.th class="text-right">Totals</x-table.th>
<x-table.th>${{ number_format($invoiceData['linesTotal'],2) }}</x-table.th>
<x-table.th>${{ number_format($invoiceData['lateFeesTotal'],2) }}</x-table.th>
</tr>
<tr>
<td colspan="3"></td>
<x-table.th class="text-right">Total Entry Fees</x-table.th>
<x-table.th>${{ number_format($invoiceData['linesTotal'] + $invoiceData['lateFeesTotal'], 2) }}</x-table.th>
</tr>
<tr>
<x-table.th colspan="4" class="text-right">School Fee</x-table.th>
<x-table.th >${{ number_format($invoiceData['schoolFeeTotal'],2) }}</x-table.th>
</tr>
<tr>
<x-table.th colspan="4" class="text-right">Grand Total</x-table.th>
<x-table.th >${{ number_format($invoiceData['grandTotal'],2) }}</x-table.th>
</tr>
</tfoot>
</x-table.table>
</div>
</x-layout.app>

View File

@ -18,59 +18,3 @@
</x-layout.page-section> </x-layout.page-section>
</x-layout.page-section-container> </x-layout.page-section-container>
</x-layout.app> </x-layout.app>
{{--@php use Illuminate\Support\Facades\Auth; @endphp
<x-layout.app>
<x-slot:page_title>User Profile</x-slot:page_title>
<div class="space-y-10 divide-y divide-gray-900/10">
<x-layout.page-section
section_name="Personal Information"
section_description="Use a permanent address where you receive mail"
>
@if (session('status') === 'profile-information-updated')
<div class="mt-4 px-8 font-medium text-sm text-green-600">
Profile Info has been updated.
</div>
@endif
<form action="/user/profile-information" method="POST">
@csrf
@method('PUT')
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<x-form.field name="first_name" label="First Name" value="{{ Auth::user()->first_name }}" div_classes="sm:col-span-3" />
<x-form.field name="last_name" label="Last Name" value="{{ Auth::user()->last_name }}" div_classes="sm:col-span-3" />
<x-form.field name="email" label="Email Address" value="{{ Auth::user()->email }}" div_classes="sm:col-span-3" />
<x-form.field name="cell_phone" label="Cell Phone" value="{{ Auth::user()->cell_phone }}" div_classes="sm:col-span-3" />
<x-form.field name="judging_preference" label="Judging Preference" value="{{ Auth::user()->judging_preference }}" div_classes="sm:col-span-5" />
</div>
</div>
<div class="flex items-center justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<x-form.button-nocolor type="button">Cancel</x-form.button-nocolor>
<x-form.button>Update Profile</x-form.button>
</div>
</form>
</x-layout.page-section>
<x-layout.page-section
section_name="Change Password"
section_description="Update your user password"
>
@if (session('status') === 'password-updated')
<div class="mt-4 px-8 font-medium text-sm text-green-600">
Password has been updated.
</div>
@endif
<x-form.card method="PUT" action="/user/password" cols="1" submit-button-text="Change Password">
<x-form.field name="current_password" label="Current Password" type="password" autocomplete="current-password" required />
<x-form.field name="password" label="New Password" type="password" autocomplete="new-password" required />
<x-form.field name="password_confirmation" label="Confirm New Password" autocomplete="new-password" type="password" required />
</x-form.card>
</x-layout.page-section>
</div>
</x-layout.app>--}}

View File

@ -10,18 +10,16 @@
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
@endphp @endphp
@inject('scoreservice','App\Services\ScoreService'); @inject('scoreservice','App\Services\ScoreService');
@inject('auditionService','App\Services\AuditionCacheService'); @inject('auditionService','App\Services\AuditionService');
@inject('entryService','App\Services\EntryCacheService') @inject('entryService','App\Services\EntryService')
@inject('seatingService','App\Services\SeatingService') @inject('seatingService','App\Services\SeatingService')
<x-layout.app> <x-layout.app>
<x-slot:page_title>Test Page</x-slot:page_title> <x-slot:page_title>Test Page</x-slot:page_title>
@php @php
$schools = School::all(); dump($totalFees);
dump($lines);
}
@endphp @endphp
</x-layout.app> </x-layout.app>

View File

@ -99,6 +99,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::get('/create', 'create')->name('admin.schools.create'); Route::get('/create', 'create')->name('admin.schools.create');
Route::get('/{school}', 'show')->name('admin.schools.show'); Route::get('/{school}', 'show')->name('admin.schools.show');
Route::get('/{school}/edit', 'edit')->name('admin.schools.edit'); Route::get('/{school}/edit', 'edit')->name('admin.schools.edit');
Route::get('/{school}/invoice', 'viewInvoice')->name('admin.schools.invoice');
Route::patch('/{school}', 'update')->name('admin.schools.update'); Route::patch('/{school}', 'update')->name('admin.schools.update');
Route::post('/', 'store')->name('admin.schools.store'); Route::post('/', 'store')->name('admin.schools.store');
Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain'); Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain');

View File

@ -10,8 +10,9 @@ use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'dashboard'])->name('dashboard'); Route::get('/dashboard', [DashboardController::class, 'dashboard'])->name('dashboard');
Route::get('/profile', [DashboardController::class, 'profile']); Route::get('/profile', [DashboardController::class, 'profile'])->name('my_profile');
Route::get('/my_school', [DashboardController::class, 'my_school']); Route::get('/my_school', [DashboardController::class, 'my_school'])->name('my_school');
Route::get('/my_invoice', [DashboardController::class, 'my_invoice'])->name('my_invoice');
}); });
// Entry Related Routes // Entry Related Routes