Skip to content

Commit

Permalink
feat: notify project closure & verify due date
Browse files Browse the repository at this point in the history
  • Loading branch information
keenthekeen committed Aug 3, 2024
1 parent d8ee496 commit d053959
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 8 deletions.
9 changes: 8 additions & 1 deletion app/Http/Controllers/Dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

namespace App\Http\Controllers;

use App\Models\ProjectParticipant;
use Illuminate\Http\Request;
use Inertia\Inertia;

class Dashboard extends Controller {
public function __invoke(Request $request) {
return Inertia::render('Dashboard', [
// 'projectsAwaitingSummary' => $request->user()->projects()->select(['id', 'number', 'year', 'name'])->whereDate('created_at', '>', now()->subYear())->whereNotIn('department_id', [32, 38, 39])->has('approvalDocument')->doesntHave('summaryDocument')->get(),
'myProjects' => $request->user()->participantAndProjects(),
'myProjects' => $request->user()->participantAndProjects()->map(function (ProjectParticipant $participant) {
$participant->project->closure_status = $participant->project->getClosureStatus();
$participant->verify_status = !empty($participant->verify_status);
$participant->makeVisible('verify_status');

return $participant;
}),
]);
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/ProjectClosureController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers;

use App\Jobs\NotifyProjectVerifyJob;
use App\Models\Department;
use App\Models\Project;
use App\Models\ProjectParticipant;
Expand Down Expand Up @@ -59,6 +60,8 @@ public function closureSubmit(Request $request, Project $project) {

if ($action == 'generate_document') {
return Inertia::location(route('projects.generateSummaryDocument', ['project' => $project->id]));
} elseif ($project->closure_submitted_at) {
NotifyProjectVerifyJob::dispatch($project)->delay(now()->addMinutes(30));
}

return redirect()
Expand Down
31 changes: 31 additions & 0 deletions app/Jobs/NotifyProjectClosureDueJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Jobs;

use App\Models\Project;
use App\Notifications\ClosureDueNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class NotifyProjectClosureDueJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function handle(): void {
// Start sending notification after 2 days of project end time
Project::where('created_at', '>=', now()->subYear())
->where('year', '>=', 2567)
->where('period_end', '<=', now()->subDays(2))
->whereNull('closure_reminded_at')
->get()
->filter(fn(Project $project) => $project->canSubmitClosure() and $project->documents()->where('tag', 'summary')->isEmpty())
->each(function (Project $project) {
$project->participants()->with('user')->get()
->reject(fn($participant) => $participant->type == 'attendee' and $participant->user?->student_id < 6700000000)
->each(fn($participant) => $participant->user->notify(new ClosureDueNotification($project, $participant)));
$project->update(['closure_reminded_at' => now()]);
});
}
}
39 changes: 39 additions & 0 deletions app/Jobs/NotifyProjectVerifyJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace App\Jobs;

use App\Models\Project;
use App\Models\ProjectParticipant;
use App\Notifications\ClosureVerifyNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class NotifyProjectVerifyJob implements ShouldQueue, ShouldBeUnique {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public int $uniqueFor = 7200;

public function __construct(public Project $project) {
}

public function handle(): void {
$this->project->participants()->whereIn('type', ['organizer', 'staff'])->where('verify_status', 0)->get()
->each(fn(ProjectParticipant $participant) => $participant->user->notify(new ClosureVerifyNotification($this->project)));
}

/**
* Get the unique ID for the job.
*/
public function uniqueId(): string {
return $this->project->id;
}
}
37 changes: 37 additions & 0 deletions app/Notifications/ClosureDueNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Notifications;

use App\Models\Project;
use App\Models\ProjectParticipant;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class ClosureDueNotification extends Notification implements ShouldQueue {
use Queueable;

public function __construct(public Project $project, public ProjectParticipant $participant) {
}

public function via($notifiable): array {
return ['mail'];
}

public function toMail($notifiable): MailMessage {
return (new MailMessage)
->subject('กรุณาบันทึกรายงานผล โครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name)
->greeting('ได้เวลาบันทึกรายงานผล โครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name.' แล้ว!')
->line('เรียน นิสิตผู้รับผิดชอบโครงการ')
->line('เมื่อเสร็จสิ้นโครงการแล้ว ให้รายงานผลการดำเนินโครงการ และส่งเบิกค่าใช้จ่าย (ถ้ามี) ให้เรียบร้อยโดยเร็ว')
->lineIf($this->participant->type != 'organizer', 'คุณไม่ใช่ผู้รับผิดชอบโครงการ กรุณาแจ้งนิสิตผู้รับผิดชอบโครงการให้ดำเนินการต่อไป')
->action('ดูข้อมูลโครงการเพิ่มเติม', route('projects.show', ['project' => $this->project->id]))
->line('โครงการที่จะบันทึกเป็นส่วนหนึ่งของ Activity Transcript (Student Profile) ต้องบันทึกรายงานผลการดำเนินโครงการ และกดยืนยันการส่ง ภายใน 30 วัน
นับจากวันที่สิ้นสุดกิจกรรม ('.$this->project->period_end->format('j M Y').')');
}

public function toArray($notifiable): array {
return ['ได้เวลาบันทึกรายงานผล โครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name.' แล้ว!'];
}
}
34 changes: 34 additions & 0 deletions app/Notifications/ClosureVerifyNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Notifications;

use App\Models\Project;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class ClosureVerifyNotification extends Notification implements ShouldQueue {
use Queueable;

public function __construct(public Project $project) {
}

public function via(): array {
return ['mail'];
}

public function toMail(User $notifiable): MailMessage {
return (new MailMessage)
->subject('กรุณารับรองรายชื่อนิสิต โครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name)
->greeting('เรียน '.$notifiable->name)
->line('กรุณารับรองรายชื่อนิสิตผู้เกี่ยวข้อง ของโครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name)
->line('โครงการที่จะบันทึกเป็นส่วนหนึ่งของ Activity Transcript ต้องผ่านการรับรองรายชื่อนิสิตผู้เกี่ยวข้อง โดยนิสิตผู้รับผิดชอบและผู้ปฏิบัติงานทุกคนภายใน 60 วัน นับจากสิ้นสุดกิจกรรม ('.$this->project->period_end->format('j M Y').')')
->action('รับรองรายชื่อนิสิตผู้เกี่ยวข้อง', route('projects.closureVerifyForm', ['project' => $this->project->id]));
}

public function toArray(): array {
return ['กรุณารับรองรายชื่อนิสิต โครงการที่ '.$this->project->year.'-'.$this->project->number.' '.$this->project->name];
}
}
44 changes: 37 additions & 7 deletions resources/js/Pages/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import Welcome from '@/Jetstream/Welcome.vue';
import {PROJECT_PARTICIPANT_ROLES} from "@/static";
import {PROJECT_PARTICIPANT_ROLES} from '@/static';
import {Link} from '@inertiajs/vue3';
const props = defineProps({myProjects: Array});
const lastYear = new Date();
lastYear.setMonth(lastYear.getMonth() - 18);
const participants = props.myProjects.map(participant => {
participant.project.awaitingSummary = participant.project.approval_document && !participant.project.summary_document && ![32, 38, 39].includes(participant.project.department_id) && (new Date(participant.project.created_at) > lastYear);
participant.project.awaitingSummaryAlert = participant.project.awaitingSummary && (new Date(participant.project.created_at) > lastYear) && (participant.type === 'organizer');
participant.project.awaitingVerify = [1, 5].includes(participant.project.closure_status) && ['organizer', 'staff'].includes(participant.type) && !participant.verify_status;
return participant;
});
const projectsAwaitingSummary = props.myProjects.map(participant => participant.project).filter(project => project.awaitingSummaryAlert);
const projectsAwaitingSummary = participants.map(participant => participant.project).filter(project => project.awaitingSummaryAlert);
const projectsAwaitingVerify = participants.map(participant => participant.project).filter(project => project.awaitingVerify);
</script>
<template>
<app-layout>
Expand All @@ -22,6 +25,31 @@ const projectsAwaitingSummary = props.myProjects.map(participant => participant.
</template>

<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 pt-12">
<div v-if="projectsAwaitingVerify.length > 0"
class="animate-pulse bg-purple-100 border-purple-600 text-purple-600 border-l-4 rounded p-4 mb-6">
<p class="font-bold">
กรุณารับรองรายชื่อนิสิตผู้เกี่ยวข้อง
</p>
<p>
โครงการที่จะบันทึกเป็นส่วนหนึ่งของ Activity Transcript ต้องผ่านการรับรองรายชื่อนิสิตผู้เกี่ยวข้อง
โดยนิสิตผู้รับผิดชอบและผู้ปฏิบัติงานทุกคนภายใน 60 วัน นับจากสิ้นสุดกิจกรรม
</p>
<table class="text-sm">
<tr v-for="project in projectsAwaitingVerify">
<td>•</td>
<td class="px-2">
<Link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600 text-xs">
{{ project.year }}-{{ project.number }}
</Link>
</td>
<td>
<Link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600">
{{ project.name }}
</Link>
</td>
</tr>
</table>
</div>
<div v-if="projectsAwaitingSummary.length > 0" class="bg-blue-100 border-blue-500 text-blue-500 border-l-4 rounded p-4 mb-6" role="alert">
<p class="font-bold">
มี {{ projectsAwaitingSummary.length }} โครงการที่กำลังดำเนินงานอยู่
Expand All @@ -31,14 +59,14 @@ const projectsAwaitingSummary = props.myProjects.map(participant => participant.
<tr v-for="project in projectsAwaitingSummary">
<td>•</td>
<td class="px-2">
<inertia-link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600 text-xs">
<Link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600 text-xs">
{{ project.year }}-{{ project.number }}
</inertia-link>
</Link>
</td>
<td>
<inertia-link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600">
<Link :href="route('projects.show', {project: project.id})" class="hover:text-blue-600">
{{ project.name }}
</inertia-link>
</Link>
</td>
</tr>
</table>
Expand All @@ -62,7 +90,9 @@ const projectsAwaitingSummary = props.myProjects.map(participant => participant.
<div class="items-center md:flex gap-4">
<p :class="{'text-gray-400': participant.project.awaitingSummary, 'text-gray-900': !participant.project.awaitingSummary}"
class="flex-auto font-medium">
<span class="text-xs text-gray-500 px-0.5">{{ participant.project.year }}-{{ participant.project.number }}</span>
<Link :href="route('projects.show', {project: participant.project.id})" class="text-xs text-gray-500 px-0.5">
{{ participant.project.year }}-{{ participant.project.number }}
</Link>
{{ participant.project.name }}
</p>
<p class="flex-auto text-sm text-gray-500 md:text-right">
Expand Down
58 changes: 58 additions & 0 deletions resources/views/vendor/notifications/email.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<x-mail::message>
{{-- Greeting --}}
@if (! empty($greeting))
# {{ $greeting }}
@else
@if ($level === 'error')
# @lang('Whoops!')
@else
# @lang('Hello!')
@endif
@endif

{{-- Intro Lines --}}
@foreach ($introLines as $line)
{{ $line }}

@endforeach

{{-- Action Button --}}
@isset($actionText)
<?php
$color = match ($level) {
'success', 'error' => $level,
default => 'primary',
};
?>
<x-mail::button :url="$actionUrl" :color="$color">
{{ $actionText }}
</x-mail::button>
@endisset

{{-- Outro Lines --}}
@foreach ($outroLines as $line)
{{ $line }}

@endforeach

{{-- Salutation --}}
@if (! empty($salutation))
{{ $salutation }}
@else
ขอแสดงความนับถือ<br>
ระบบบริหารงานสโมสร สโมสรนิสิตคณะแพทยศาสตร์ จุฬาลงกรณ์มหาวิทยาลัย
@endif

{{-- Subcopy --}}
@isset($actionText)
<x-slot:subcopy>
@lang(
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n".
'into your web browser:',
[
'actionText' => $actionText,
]
) <span class="break-all">[{{ $displayableActionUrl }}]({{ $actionUrl }})</span>
</x-slot:subcopy>
@endisset
</x-mail::message>
3 changes: 3 additions & 0 deletions routes/console.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use App\Console\Commands\FetchEmailCommand;
use App\Jobs\NotifyProjectClosureDueJob;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Spatie\Health\Commands\ScheduleCheckHeartbeatCommand;
Expand All @@ -12,3 +13,5 @@
Schedule::command(FetchEmailCommand::class)
->everyFourHours()->withoutOverlapping(28800)->appendOutputTo(storage_path('logs/fetch-email.log'));;
Schedule::command(ScheduleCheckHeartbeatCommand::class)->everyFiveMinutes();

Schedule::job(new NotifyProjectClosureDueJob)->daily();

0 comments on commit d053959

Please sign in to comment.