-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolves #375 --------- Co-authored-by: Thor K. Høgås <[email protected]>
- Loading branch information
Showing
34 changed files
with
1,172 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?php | ||
|
||
namespace App\Console\Commands; | ||
|
||
use App\Helpers\TaskStatus; | ||
use App\Models\Task; | ||
use App\Models\User; | ||
use App\Notifications\TaskNotification; | ||
use Illuminate\Console\Command; | ||
|
||
class SendTaskNotifications extends Command | ||
{ | ||
/** | ||
* The name and signature of the console command. | ||
* | ||
* @var string | ||
*/ | ||
protected $signature = 'send:task:notifications'; | ||
|
||
/** | ||
* The console command description. | ||
* | ||
* @var string | ||
*/ | ||
protected $description = 'Send out the digest of task notifications'; | ||
|
||
/** | ||
* Execute the console command. | ||
* | ||
* @return int | ||
*/ | ||
public function handle() | ||
{ | ||
|
||
// For recipients who have not yet been notified | ||
$pendingTasks = Task::where('status', TaskStatus::PENDING)->where('assignee_notified', false)->get(); | ||
|
||
// For senders who have not yet been notified | ||
$completedTasks = Task::where('status', TaskStatus::COMPLETED)->where('creator_notified', false)->get(); | ||
$declinedTasks = Task::where('status', TaskStatus::DECLINED)->where('creator_notified', false)->get(); | ||
|
||
// Put together the list of email recipients | ||
$tasks = $pendingTasks->merge($completedTasks)->merge($declinedTasks); | ||
$usersRecipients = $pendingTasks->pluck('assignee_user_id')->merge($completedTasks->pluck('creator_user_id'))->merge($declinedTasks->pluck('creator_user_id'))->unique(); | ||
$userModels = User::whereIn('id', $usersRecipients)->get(); | ||
|
||
foreach ($userModels as $user) { | ||
|
||
// If no tasks for this user, skip | ||
if (! $tasks->where('assignee_user_id', $user->id)->count() && ! $tasks->where('creator_user_id', $user->id)->count()) { | ||
continue; | ||
} | ||
|
||
// If user has disabled task notifications, mark as notified and skip | ||
if (! $user->setting_notify_tasks) { | ||
|
||
$tasks->where('assignee_user_id', $user->id)->each(function ($task) { | ||
$task->assignee_notified = true; | ||
$task->save(); | ||
}); | ||
|
||
$tasks->where('creator_user_id', $user->id)->each(function ($task) { | ||
$task->creator_notified = true; | ||
$task->save(); | ||
}); | ||
|
||
continue; | ||
} | ||
|
||
$user->notify(new TaskNotification( | ||
$user, | ||
$tasks->where('assignee_user_id', $user->id), | ||
$tasks->where('creator_user_id', $user->id)->whereIn('status', [TaskStatus::COMPLETED, TaskStatus::DECLINED]), | ||
)); | ||
} | ||
|
||
return Command::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace App\Helpers; | ||
|
||
/** | ||
* Constants for task status. | ||
*/ | ||
enum TaskStatus: int | ||
{ | ||
case PENDING = 0; | ||
case DECLINED = -1; | ||
case COMPLETED = 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php | ||
|
||
namespace App\Http\Controllers; | ||
|
||
use App\Helpers\TaskStatus; | ||
use App\Models\Task; | ||
use App\Models\User; | ||
use App\Rules\ValidTaskType; | ||
use Illuminate\Contracts\Auth\Authenticatable; | ||
use Illuminate\Http\RedirectResponse; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\File; | ||
use Illuminate\View\View; | ||
|
||
class TaskController extends Controller | ||
{ | ||
/** | ||
* Show the application task dashboard. | ||
*/ | ||
public function index(Authenticatable $user, string $activeFilter = null): View | ||
{ | ||
$this->authorize('update', Task::class); | ||
|
||
if ($activeFilter == 'sent') { | ||
$tasks = Task::where('creator_user_id', $user->id)->get()->sortBy('created_at'); | ||
} elseif ($activeFilter == 'archived') { | ||
$tasks = Task::where('assignee_user_id', $user->id)->whereIn('status', [TaskStatus::COMPLETED->value, TaskStatus::DECLINED->value])->get()->sortBy('created_at'); | ||
} else { | ||
$tasks = Task::where('assignee_user_id', $user->id)->where('status', TaskStatus::PENDING->value)->with('creator', 'subject', 'assignee', 'subjectTraining')->get()->sortBy('created_at'); | ||
} | ||
|
||
return view('tasks.index', compact('tasks', 'activeFilter')); | ||
} | ||
|
||
/** | ||
* Store a newly created task in storage. | ||
*/ | ||
public function store(Request $request, Authenticatable $user): RedirectResponse | ||
{ | ||
|
||
$this->authorize('create', Task::class); | ||
|
||
$data = $request->validate([ | ||
'type' => ['required', new ValidTaskType], | ||
'message' => 'sometimes|min:3|max:256', | ||
'subject_user_id' => 'required|exists:users,id', | ||
'subject_training_id' => 'required|exists:trainings,id', | ||
'assignee_user_id' => 'required|exists:users,id', | ||
]); | ||
|
||
$data['creator_user_id'] = $user->id; | ||
$data['created_at'] = now(); | ||
|
||
// Check if recipient is mentor or above | ||
$recipient = User::findOrFail($data['assignee_user_id']); | ||
|
||
// Policy check if recpient can recieve a task | ||
if ($recipient->can('receive', Task::class)) { | ||
// Create the model | ||
$task = Task::create($data); | ||
|
||
// Run the create method on the task type to trigger type specific actions on creation | ||
$task->type()->create($task); | ||
|
||
return redirect()->back()->with('success', 'Task created successfully.'); | ||
} | ||
|
||
return redirect()->back()->withErrors('Recipient is not allowed to receive tasks.'); | ||
|
||
} | ||
|
||
/** | ||
* Close the specified task with a given status. | ||
*/ | ||
protected function close(Task|int $task, TaskStatus $status): Task | ||
{ | ||
$this->authorize('update', Task::class); | ||
$task = Task::findOrFail($task); | ||
$task->status = $status; | ||
$task->closed_at = now(); | ||
$task->save(); | ||
|
||
return $task; | ||
} | ||
|
||
/** | ||
* Complete the specified task. | ||
*/ | ||
public function complete(Request $request, Task|int $task): RedirectResponse | ||
{ | ||
$completed = self::close($task, TaskStatus::COMPLETED); | ||
|
||
// Run the complete method on the task type to trigger type specific actions on completion | ||
$completed->type()->complete($completed); | ||
|
||
return redirect()->back()->with('success', sprintf('Completed task regarding %s from %s.', $completed->subject->name, $completed->creator->name)); | ||
} | ||
|
||
/** | ||
* Decline the specified task. | ||
*/ | ||
public function decline(Request $request, Task|int $task): RedirectResponse | ||
{ | ||
$declined = self::close($task, TaskStatus::DECLINED); | ||
|
||
// Run the decline method on the task type to trigger type specific actions on decline | ||
$declined->type()->decline($declined); | ||
|
||
return redirect()->back()->with('success', sprintf('Declined task regarding %s from %s.', $declined->subject->name, $declined->creator->name)); | ||
} | ||
|
||
/** | ||
* Return the task type classes | ||
* | ||
* @return array | ||
*/ | ||
public static function getTypes() | ||
{ | ||
// Specify the directory where your subclasses are located | ||
$subclassesDirectory = app_path('Tasks/Types'); | ||
|
||
// Initialize an array to store the subclasses | ||
$subclasses = []; | ||
|
||
// Get all PHP files in the directory | ||
$files = File::files($subclassesDirectory); | ||
|
||
foreach ($files as $file) { | ||
// Get the class name from the file path | ||
$className = 'App\\Tasks\\Types\\' . pathinfo($file, PATHINFO_FILENAME); | ||
|
||
// Check if the class exists and is a subclass of Types | ||
if (class_exists($className) && is_subclass_of($className, 'App\\Tasks\\Types\\Types')) { | ||
$subclasses[] = new $className(); | ||
} | ||
} | ||
|
||
return $subclasses; | ||
} | ||
|
||
/** | ||
* Check if a task type is valid | ||
* | ||
* @param string $type | ||
* @return bool | ||
*/ | ||
public static function isValidType($type) | ||
{ | ||
$types = self::getTypes(); | ||
|
||
foreach ($types as $taskType) { | ||
if ($taskType::class == $type) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php | ||
|
||
namespace App\Mail; | ||
|
||
use App\Models\User; | ||
use Illuminate\Bus\Queueable; | ||
use Illuminate\Mail\Mailable; | ||
use Illuminate\Queue\SerializesModels; | ||
|
||
class TaskMail extends Mailable | ||
{ | ||
use Queueable, SerializesModels; | ||
|
||
private $mailSubject; | ||
|
||
private $user; | ||
|
||
private $textLines; | ||
|
||
/** | ||
* Create a new message instance. | ||
* | ||
* @param string $mailSubject the subject of the email | ||
* @param User $user the user to send the email to | ||
* @param array $textLines an array of markdown lines to add | ||
* @return void | ||
*/ | ||
public function __construct(string $mailSubject, User $user, array $textLines) | ||
{ | ||
$this->mailSubject = $mailSubject; | ||
$this->user = $user; | ||
$this->textLines = $textLines; | ||
} | ||
|
||
/** | ||
* Build the message. | ||
* | ||
* @return $this | ||
*/ | ||
public function build() | ||
{ | ||
return $this->subject($this->mailSubject)->markdown('mail.tasks', [ | ||
'firstName' => $this->user->first_name, | ||
'textLines' => $this->textLines, | ||
'actionUrl' => route('tasks'), | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<?php | ||
|
||
namespace App\Models; | ||
|
||
use App\Helpers\TaskStatus; | ||
use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
use Illuminate\Database\Eloquent\Model; | ||
|
||
class Task extends Model | ||
{ | ||
use HasFactory; | ||
|
||
protected $guarded = []; | ||
|
||
protected $casts = [ | ||
'status' => TaskStatus::class, | ||
]; | ||
|
||
public function creator() | ||
{ | ||
return $this->belongsTo(User::class, 'creator_user_id'); | ||
} | ||
|
||
public function subject() | ||
{ | ||
return $this->belongsTo(User::class, 'subject_user_id'); | ||
} | ||
|
||
public function subjectTraining() | ||
{ | ||
return $this->belongsTo(Training::class, 'subject_training_id'); | ||
} | ||
|
||
public function assignee() | ||
{ | ||
return $this->belongsTo(User::class, 'assignee_user_id'); | ||
} | ||
|
||
public function type() | ||
{ | ||
if ($this->type) { | ||
return app($this->type); | ||
} else { | ||
throw new \Exception('Invalid task type: ' . $this->type); | ||
} | ||
} | ||
} |
Oops, something went wrong.