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\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\IncrementAvailabilityMinutesJob;
use App\Jobs\UpdateAvailabilityWithSchedulesJob;
class Kernel extends ConsoleKernel
{
@ -14,6 +15,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
$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 App\Http\Controllers\AuthController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\ScheduleSlotsController;
use App\Http\Controllers\AvailabilityController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
@ -27,6 +28,9 @@ Route::middleware('auth:sanctum')->group( function () {
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::post('/availability', [AvailabilityController::class, 'updateAvailability']);
Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']);

View File

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

View File

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

View File

@ -13,7 +13,7 @@
</button>
<br>
</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 }}
</button>
</div>