Implement availability schedules

This commit is contained in:
Matteo Gheza 2023-03-15 23:09:02 +01:00
parent 6580fc1bde
commit c218e44b4e
9 changed files with 229 additions and 81 deletions

View File

@ -5,6 +5,7 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\IncrementAvailabilityMinutesJob; use App\Jobs\IncrementAvailabilityMinutesJob;
use App\Jobs\UpdateAvailabilityWithSchedulesJob;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@ -14,6 +15,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
$schedule->job(new IncrementAvailabilityMinutesJob)->everyMinute(); $schedule->job(new IncrementAvailabilityMinutesJob)->everyMinute();
$schedule->job(new UpdateAvailabilityWithSchedulesJob)->everyThirtyMinutes();
} }
/** /**

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers;
use App\Models\ScheduleSlots;
use Illuminate\Http\Request;
class ScheduleSlotsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
return ScheduleSlots::select("day", "slot")
->where('user', $request->user()->id)
->get();
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
ScheduleSlots::where('user', $request->user()->id)->delete();
$schedules = array_map(function ($schedule) {
$schedule["user"] = auth()->id();
return $schedule;
}, $request->input('schedules'));
return ScheduleSlots::insert($schedules);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\ScheduleSlots;
use App\Models\User;
class UpdateAvailabilityWithSchedulesJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
//Days starts from 0 in frontend
$curr_day = now()->dayOfWeek-1;
//There are 48 slots of 30 minutes, starting from 0 (00:00-00:30) to 47 (23:30-00:00)
$curr_slot = now()->hour * 2 + (now()->minute >= 30);
$scheduled_users = ScheduleSlots::where([
["day", "=", $curr_day],
["slot", "=", $curr_slot]
])->pluck("user");
User::whereIn("id", $scheduled_users)
->where([
["banned", "=", 0],
["availability_manual_mode", "=", 0]
])
->update(['available' => 1]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ScheduleSlots extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'day',
'slot'
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
];
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
protected static function defAttr($messages, $attribute){
if(isset($messages[$attribute])){
return $messages[$attribute];
}
$attributes = [
"user" => auth()->id(),
];
return $attributes[$attribute];
}
protected static function booted()
{
static::creating(function ($messages) {
$messages->user = self::defAttr($messages, "user");
});
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('schedule_slots', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('day');
$table->unsignedTinyInteger('slot');
$table->unsignedBigInteger('user')->unsigned();
$table->foreign('user')->references('id')->on('users')->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('schedule_slots');
}
};

View File

@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\ScheduleSlotsController;
use App\Http\Controllers\AvailabilityController; use App\Http\Controllers\AvailabilityController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
@ -27,6 +28,9 @@ Route::middleware('auth:sanctum')->group( function () {
Route::get('/list', [UserController::class, 'index']); Route::get('/list', [UserController::class, 'index']);
Route::get('/schedules', [ScheduleSlotsController::class, 'index']);
Route::post('/schedules', [ScheduleSlotsController::class, 'store']);
Route::get('/availability', [AvailabilityController::class, 'get']); Route::get('/availability', [AvailabilityController::class, 'get']);
Route::post('/availability', [AvailabilityController::class, 'updateAvailability']); Route::post('/availability', [AvailabilityController::class, 'updateAvailability']);
Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']); Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']);

View File

@ -11,22 +11,22 @@
<td style="background-color: white;"></td> <td style="background-color: white;"></td>
<ng-container *ngIf="orientation === 'portrait'"> <ng-container *ngIf="orientation === 'portrait'">
<ng-container *ngFor="let day of days; let i = index"> <ng-container *ngFor="let day of days; let i = index">
<td class="day" (click)="selectDay(i)">{{ day.short|translate }}</td> <td class="day" (click)="selectEntireDay(i)">{{ day.short|translate }}</td>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="orientation === 'landscape'"> <ng-container *ngIf="orientation === 'landscape'">
<ng-container *ngFor="let hour of hours"> <ng-container *ngFor="let slot of slots; odd as isOdd; even as isEven;">
<td class="hour" (click)="selectHour(hour)">{{ hour }}</td> <td class="hour" (click)="selectEverySlotWithHour(slot)">{{ isEven ? (slot/2) : ((slot-1)/2) }}:{{ isEven ? "00" : "30" }}</td>
</ng-container> </ng-container>
</ng-container> </ng-container>
</tr> </tr>
</thead> </thead>
<tbody id="scheduler_body" *ngIf="orientation === 'portrait'"> <tbody id="scheduler_body" *ngIf="orientation === 'portrait'">
<ng-container *ngFor="let hour of hours"> <ng-container *ngFor="let slot of slots">
<tr> <tr>
<td class="hour" (click)="selectHour(hour)">{{ hour }}</td> <td class="hour" (click)="selectEverySlotWithHour(slot)">{{ slot }}</td>
<ng-container *ngFor="let day of days; let i = index"> <ng-container *ngFor="let day of days; let i = index">
<td class="hour-cell" [class.highlighted] = "isCellSelected(i, hour)" (mousedown)="mouseDownCell(i, hour)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, hour)"></td> <td class="hour-cell" [class.highlighted] = "isCellSelected(i, slot)" (mousedown)="mouseDownCell(i, slot)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, slot)"></td>
</ng-container> </ng-container>
</tr> </tr>
</ng-container> </ng-container>
@ -34,9 +34,9 @@
<tbody id="scheduler_body" *ngIf="orientation === 'landscape'"> <tbody id="scheduler_body" *ngIf="orientation === 'landscape'">
<ng-container *ngFor="let day of days; let i = index"> <ng-container *ngFor="let day of days; let i = index">
<tr> <tr>
<td class="day" (click)="selectDay(i)">{{ day.short|translate }}</td> <td class="day" (click)="selectEntireDay(i)">{{ day.short|translate }}</td>
<ng-container *ngFor="let hour of hours"> <ng-container *ngFor="let slot of slots">
<td class="hour-cell" [class.highlighted] = "isCellSelected(i, hour)" (mousedown)="mouseDownCell(i, hour)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, hour)"></td> <td class="hour-cell" [class.highlighted] = "isCellSelected(i, slot)" (mousedown)="mouseDownCell(i, slot)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, slot)"></td>
</ng-container> </ng-container>
</tr> </tr>
</ng-container> </ng-container>

View File

@ -41,37 +41,12 @@ export class ModalAvailabilityScheduleComponent implements OnInit {
short: 'sunday_short' short: 'sunday_short'
} }
]; ];
public hours = [ public slots = Array(48).fill(0).map((x,i)=>i);
"0:00", "0:30",
"1:00", "1:30",
"2:00", "2:30",
"3:00", "3:30",
"4:00", "4:30",
"5:00", "5:30",
"6:00", "6:30",
"7:00", "7:30",
"8:00", "8:30",
"9:00", "9:30",
"10:00", "10:30",
"11:00", "11:30",
"12:00", "12:30",
"13:00", "13:30",
"14:00", "14:30",
"15:00", "15:30",
"16:00", "16:30",
"17:00", "17:30",
"18:00", "18:30",
"19:00", "19:30",
"20:00", "20:30",
"21:00", "21:30",
"22:00", "22:30",
"23:00", "23:30",
];
public selectedCells: any = []; public selectedCells: any = [];
//Used for "select all" //Used for "select all"
public selectedHours: string[] = []; public selectedSlots: number[] = [];
public selectedDays: number[] = []; public selectedDays: number[] = [];
public isSelecting = false; public isSelecting = false;
@ -91,13 +66,9 @@ export class ModalAvailabilityScheduleComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape"; this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape";
if(localStorage.getItem('schedules') === null) { this.api.get("schedules").then((response: any) => {
this.api.get("schedules").then((response: any) => { this.loadSchedules(response);
this.loadSchedules(response.schedules); });
});
} else {
this.loadSchedules(JSON.parse((localStorage.getItem('schedules') as string)));
}
} }
saveChanges() { saveChanges() {
@ -105,79 +76,72 @@ export class ModalAvailabilityScheduleComponent implements OnInit {
this.api.post("schedules", { this.api.post("schedules", {
schedules: this.selectedCells schedules: this.selectedCells
}); });
localStorage.removeItem('schedules');
this.bsModalRef.hide(); this.bsModalRef.hide();
} }
saveChangesInLocal() {
localStorage.setItem('schedules', JSON.stringify(this.selectedCells));
}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(event: Event) { onResize(event: Event) {
this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape"; this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape";
} }
isCellSelected(day: number, hour: string) { isCellSelected(day: number, slot: number) {
return this.selectedCells.find((cell: any) => cell.day === day && cell.hour === hour); return this.selectedCells.find((cell: any) => cell.day === day && cell.slot === slot);
} }
toggleCell(day: number, hour: string) { toggleCell(day: number, slot: number) {
if(!this.isCellSelected(day, hour)) { if(!this.isCellSelected(day, slot)) {
this.selectedCells.push({ this.selectedCells.push({
day, hour day, slot
}); });
} else { } else {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.hour !== hour); this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.slot !== slot);
} }
this.saveChangesInLocal();
} }
selectHour(hour: string) { selectEverySlotWithHour(slot: number) {
console.log("Hour selected", hour); console.log("Slot hour selected", slot);
if(this.selectedHours.includes(hour)) { debugger;
if(this.selectedSlots.includes(slot)) {
this.days.forEach((day: any, i: number) => { this.days.forEach((day: any, i: number) => {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== i || cell.hour !== hour); this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== i || cell.slot !== slot);
}); });
this.selectedHours = this.selectedHours.filter((h: string) => h !== hour); this.selectedSlots = this.selectedSlots.filter((h: number) => h !== slot);
} else { } else {
this.days.forEach((day: any, i: number) => { this.days.forEach((day: any, i: number) => {
if(!this.isCellSelected(i, hour)) { if(!this.isCellSelected(i, slot)) {
this.selectedCells.push({ this.selectedCells.push({
day: i, hour day: i, slot
}); });
} }
}); });
this.selectedHours.push(hour); this.selectedSlots.push(slot);
} }
this.saveChangesInLocal();
} }
selectDay(day: number) { selectEntireDay(day: number) {
console.log("Day selected", day); console.log("Day selected", day);
if(this.selectedDays.includes(day)) { if(this.selectedDays.includes(day)) {
this.hours.forEach((hour: string) => { for (let slot = 0; slot < 48; slot++) {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.hour !== hour); this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.slot !== slot);
}); }
this.selectedDays = this.selectedDays.filter((i: number) => i !== day); this.selectedDays = this.selectedDays.filter((i: number) => i !== day);
} else { } else {
this.hours.forEach((hour: string) => { for (let slot = 0; slot < 48; slot++) {
if(!this.isCellSelected(day, hour)) { if(!this.isCellSelected(day, slot)) {
this.selectedCells.push({ this.selectedCells.push({
day, hour day, slot
}); });
} }
}); }
this.selectedDays.push(day); this.selectedDays.push(day);
} }
this.saveChangesInLocal();
} }
mouseDownCell(day: number, hour: string) { mouseDownCell(day: number, slot: number) {
this.isSelecting = true; this.isSelecting = true;
console.log("Mouse down"); console.log("Mouse down");
console.log("Hour cell selected", day, hour); console.log("Slot cell selected", day, slot);
this.toggleCell(day, hour); this.toggleCell(day, slot);
return false; return false;
} }
@ -186,11 +150,11 @@ export class ModalAvailabilityScheduleComponent implements OnInit {
console.log("Mouse up"); console.log("Mouse up");
} }
mouseOverCell(day: number, hour: string) { mouseOverCell(day: number, slot: number) {
if (this.isSelecting) { if (this.isSelecting) {
console.log("Mouse over", day, hour); console.log("Mouse over", day, slot);
console.log("Hour cell selected", day, hour); console.log("Slot cell selected", day, slot);
this.toggleCell(day, hour); this.toggleCell(day, slot);
} }
} }

View File

@ -13,7 +13,7 @@
</button> </button>
<br> <br>
</ng-container> </ng-container>
<button type="button" class="btn btn-lg" (click)="openScheduleModal()" *ngIf="false"> <button type="button" class="btn btn-lg" (click)="openScheduleModal()">
{{ 'list.update_schedules'|translate }} {{ 'list.update_schedules'|translate }}
</button> </button>
</div> </div>