original
ajikamaludin 2 years ago
parent 112955a782
commit 02f5286ede
Signed by: ajikamaludin
GPG Key ID: 476C9A2B4B794EBB

@ -1,4 +1,4 @@
APP_NAME=Laravel APP_NAME=MonitorDoc
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
@ -8,17 +8,12 @@ LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=mysql DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=monitor_doc
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync QUEUE_CONNECTION=database
SESSION_DRIVER=file SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120

@ -2,6 +2,7 @@
namespace App\Console; namespace App\Console;
use App\Jobs\DocumentReminder;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -16,6 +17,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
// $schedule->command('inspire')->hourly(); // $schedule->command('inspire')->hourly();
$schedule->job(new DocumentReminder)->daily();
} }
/** /**

@ -2,12 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Mail\DocumentShare;
use App\Models\Department; use App\Models\Department;
use App\Models\Document; use App\Models\Document;
use App\Models\TypeDoc; use App\Models\TypeDoc;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use OpenSpout\Writer\Common\Creator\Style\StyleBuilder; use OpenSpout\Writer\Common\Creator\Style\StyleBuilder;
use Rap2hpoutre\FastExcel\FastExcel; use Rap2hpoutre\FastExcel\FastExcel;
@ -182,7 +184,7 @@ class DocumentController extends Controller
public function show(Document $doc) public function show(Document $doc)
{ {
return inertia('Document/Detail', [ return inertia('Document/Detail', [
'doc' => $doc->load(['department', 'type', 'creator', 'reminders']), 'doc' => $doc->load(['department', 'type', 'creator', 'reminders', 'shares']),
'doc_url' => asset('document/'.$doc->document), 'doc_url' => asset('document/'.$doc->document),
]); ]);
} }
@ -257,7 +259,7 @@ class DocumentController extends Controller
} else { } else {
$doc->shares()->updateOrCreate(['share_to' => $share['share_to']]); $doc->shares()->updateOrCreate(['share_to' => $share['share_to']]);
} }
// TODO: plase send email here Mail::to($share['share_to'])->queue(new DocumentShare($doc));
} }
DB::commit(); DB::commit();

@ -0,0 +1,50 @@
<?php
namespace App\Jobs;
use App\Models\Document;
use App\Mail\DocumentNotification;
use App\Models\DocumentReminder as ModelsDocumentReminder;
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;
use Illuminate\Support\Facades\Mail;
class DocumentReminder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$now = now();
$documentIds = ModelsDocumentReminder::whereDate('date', $now)->pluck('document_id');
$documents = Document::whereIn('id', $documentIds)->get();
foreach ($documents as $doc) {
Mail::to($doc->email)->queue(new DocumentNotification($doc));
if ($doc->shares()->count() > 0) {
foreach ($doc->shares as $share) {
Mail::to($share->share_to)->queue(new DocumentNotification($doc));
}
}
}
}
}

@ -0,0 +1,38 @@
<?php
namespace App\Mail;
use App\Models\Document;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class DocumentNotification extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(
public Document $doc
) {
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('emails.document.notification', [
'no_doc' => $this->doc->no_doc,
'end_date' => $this->doc->end_date->format('d-m-Y'),
'url' => route('docs.show', $this->doc)
]);
}
}

@ -0,0 +1,38 @@
<?php
namespace App\Mail;
use App\Models\Document;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class DocumentShare extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(
public Document $doc
) {
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('emails.document.share', [
'no_doc' => $this->doc->no_doc,
'end_date' => $this->doc->end_date->format('d-m-Y'),
'url' => route('docs.show', $this->doc)
]);
}
}

@ -27,9 +27,9 @@ class Document extends Model
'user_id', 'user_id',
]; ];
protected $cast = [ protected $casts = [
'start_date' => 'date', 'start_date' => 'datetime:Y-m-d',
'end_date' => 'date' 'end_date' => 'datetime:Y-m-d'
]; ];
public const ACTIVE = 0; public const ACTIVE = 0;

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('jobs');
}
};

14
package-lock.json generated

@ -10,6 +10,7 @@
"@fullcalendar/interaction": "^5.11.3", "@fullcalendar/interaction": "^5.11.3",
"@fullcalendar/react": "^5.11.2", "@fullcalendar/react": "^5.11.2",
"daisyui": "^2.28.0", "daisyui": "^2.28.0",
"moment": "^2.29.4",
"react-toastify": "^9.0.8", "react-toastify": "^9.0.8",
"react-use": "^17.4.0" "react-use": "^17.4.0"
}, },
@ -1973,6 +1974,14 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -4116,6 +4125,11 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

@ -27,6 +27,7 @@
"@fullcalendar/interaction": "^5.11.3", "@fullcalendar/interaction": "^5.11.3",
"@fullcalendar/react": "^5.11.2", "@fullcalendar/react": "^5.11.2",
"daisyui": "^2.28.0", "daisyui": "^2.28.0",
"moment": "^2.29.4",
"react-toastify": "^9.0.8", "react-toastify": "^9.0.8",
"react-use": "^17.4.0" "react-use": "^17.4.0"
} }

@ -6,17 +6,29 @@ import interactionPlugin from "@fullcalendar/interaction" // needed for dayClick
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/inertia-react'; import { Head } from '@inertiajs/inertia-react';
import { Inertia } from '@inertiajs/inertia';
export default function Dashboard(props) { export default function Dashboard(props) {
const { count_active, count_update, count_expired, count_total, events } = props const { count_active, count_update, count_expired, count_total, events } = props
console.log(events)
const calenderEvents = events.map(e => { return {title: `${e.document.no_doc} - ${e.document.pic_name}`, date: e.date} }) const calenderEvents = events.map(e => {
return {
title: `${e.document.no_doc} - ${e.document.pic_name}`,
date: e.date,
id : e.id,
url: route('docs.show', e.document)
}
})
const handleEventClick = (arg) => {
// console.log(arg.event)
}
const handleDateClick = (arg) => { // bind with an arrow function const handleDateClick = (arg) => { // bind with an arrow function
// apa yang harus di handle: tampilkan saja modal yang ada event pada date ini kemudian bisa tambah reminder atau hapus reminder pada data ini, // apa yang harus di handle: tampilkan saja modal yang ada event pada date ini kemudian bisa tambah reminder atau hapus reminder pada data ini,
// untuk tambah reminder pilih form doc id saja kemudian tambah , untuk delete cukup confirm kemudian hilang // untuk tambah reminder pilih form doc id saja kemudian tambah , untuk delete cukup confirm kemudian hilang
alert(arg.dateStr) alert(arg.dateStr)
} }
return ( return (
<AuthenticatedLayout <AuthenticatedLayout
@ -57,7 +69,7 @@ export default function Dashboard(props) {
plugins={[ dayGridPlugin, interactionPlugin ]} plugins={[ dayGridPlugin, interactionPlugin ]}
initialView="dayGridMonth" initialView="dayGridMonth"
dateClick={handleDateClick} dateClick={handleDateClick}
eventClick={(arg) => console.log(arg)} eventClick={handleEventClick}
events={calenderEvents} events={calenderEvents}
/> />
</div> </div>

@ -5,11 +5,20 @@ import DocStatusItem from './DocStatusItem'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout' import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import InputLabel from '@/Components/InputLabel' import InputLabel from '@/Components/InputLabel'
import TextInput from '@/Components/TextInput' import TextInput from '@/Components/TextInput'
import { formatDate } from '@/utils'
import ModalShare from './ModalShare'
import { useModalState } from '@/Hooks'
export default function FormDocument(props) { export default function FormDocument(props) {
const { doc, doc_url }= props const { doc, doc_url }= props
const shareModal = useModalState(false)
const handleShare = (doc) => {
shareModal.setData(doc)
shareModal.toggle()
}
return ( return (
<AuthenticatedLayout <AuthenticatedLayout
auth={props.auth} auth={props.auth}
@ -82,9 +91,9 @@ export default function FormDocument(props) {
<div className='mt-4'> <div className='mt-4'>
<InputLabel forInput="start_date" value="Tanggal Mulai" /> <InputLabel forInput="start_date" value="Tanggal Mulai" />
<TextInput <TextInput
type="date" type="text"
name="start_date" name="start_date"
value={doc.start_date} value={formatDate(doc.start_date)}
className="mt-1 block w-full" className="mt-1 block w-full"
autoComplete={"false"} autoComplete={"false"}
readOnly={true} readOnly={true}
@ -93,9 +102,9 @@ export default function FormDocument(props) {
<div className='mt-4'> <div className='mt-4'>
<InputLabel forInput="end_date" value="Tanggal Berakhir" /> <InputLabel forInput="end_date" value="Tanggal Berakhir" />
<TextInput <TextInput
type="date" type="text"
name="end_date" name="end_date"
value={doc.end_date} value={formatDate(doc.end_date)}
className="mt-1 block w-full" className="mt-1 block w-full"
autoComplete={"false"} autoComplete={"false"}
readOnly={true} readOnly={true}
@ -168,9 +177,14 @@ export default function FormDocument(props) {
</div> </div>
</div> </div>
<div className="flex items-center justify-between mt-4"> <div className="flex items-center justify-between mt-4">
<Link href={route('docs.edit', doc)} className="btn btn-outline"> <div className='flex flex-row space-x-1'>
Edit <Link href={route('docs.edit', doc)} className="btn btn-outline">
</Link> Edit
</Link>
<div className='btn btn-outline' onClick={() => handleShare(doc)}>
Share
</div>
</div>
<Link href={route('docs.index')} className="btn btn-outline"> <Link href={route('docs.index')} className="btn btn-outline">
Kembali Kembali
</Link> </Link>
@ -180,7 +194,11 @@ export default function FormDocument(props) {
</div> </div>
</div> </div>
</div> </div>
<ModalShare
isOpen={shareModal.isOpen}
toggle={shareModal.toggle}
modalState={shareModal}
/>
</AuthenticatedLayout> </AuthenticatedLayout>
) )

@ -12,6 +12,7 @@ import ModalFilter from './ModalFilter'
import ModalShare from './ModalShare' import ModalShare from './ModalShare'
import DocStatusItem from './DocStatusItem' import DocStatusItem from './DocStatusItem'
import { IconFilter, IconMenu } from '@/Icons' import { IconFilter, IconMenu } from '@/Icons'
import { formatDate } from '@/utils'
export default function Document(props) { export default function Document(props) {
const { types, departments } = props const { types, departments } = props
@ -122,7 +123,7 @@ export default function Document(props) {
<tr key={doc.id}> <tr key={doc.id}>
<td>{doc.type.name}</td> <td>{doc.type.name}</td>
<td>{doc.pic_name}</td> <td>{doc.pic_name}</td>
<td>{doc.end_date}</td> <td>{formatDate(doc.end_date)}</td>
<td><DocStatusItem status={doc.status}/></td> <td><DocStatusItem status={doc.status}/></td>
<td className='text-right'> <td className='text-right'>
<div className="dropdown dropdown-left"> <div className="dropdown dropdown-left">

@ -71,7 +71,7 @@ export default function ModalShare(props) {
<div className='py-4'> <div className='py-4'>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{shares.map((share, index) => ( {shares.map((share, index) => (
<div className="card shadow-md rounded-xl bg-slate-400 m-1" key={share.id}> <div className="card shadow-md rounded-xl bg-slate-400 m-1" key={`share-${index}`}>
<span className='flex items-center px-2 py-1'> <span className='flex items-center px-2 py-1'>
<p className='pr-1'> <p className='pr-1'>
{share.share_to} {share.share_to}

@ -1,3 +1,5 @@
import moment from "moment";
export const statuses = [ export const statuses = [
{ {
key: 0, key: 0,
@ -22,4 +24,8 @@ export const validateEmail = (email) => {
.match( .match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
); );
}; };
export const formatDate = (stringDate) => {
return moment(stringDate).format('DD-MM-yyyy')
}

@ -0,0 +1,12 @@
@component('mail::message')
# Dokumen Notifikasi
Reminder, untuk dokument <b>{{ $no_doc }}</b> akan berakhir pada {{ $end_date }} mohon untuk segera melakukan tindakan
@component('mail::button', ['url' => $url])
Detail Dokumen
@endcomponent
Terima kasih , <br>
{{ config('app.name') }}
@endcomponent

@ -0,0 +1,12 @@
@component('mail::message')
# Berbagi Dokumen
Saya membagikan dokumen dengan anda, untuk dokument <b>{{ $no_doc }}</b> akan berakhir pada {{ $end_date }} mohon untuk segera melakukan tindakan
@component('mail::button', ['url' => $url])
Detail Dokumen
@endcomponent
Terima kasih , <br>
{{ config('app.name') }}
@endcomponent
Loading…
Cancel
Save