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_KEY=
APP_DEBUG=true
@ -8,17 +8,12 @@ LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=monitor_doc
DB_USERNAME=root
DB_PASSWORD=
DB_CONNECTION=sqlite
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120

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

@ -2,12 +2,14 @@
namespace App\Http\Controllers;
use App\Mail\DocumentShare;
use App\Models\Department;
use App\Models\Document;
use App\Models\TypeDoc;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use OpenSpout\Writer\Common\Creator\Style\StyleBuilder;
use Rap2hpoutre\FastExcel\FastExcel;
@ -182,7 +184,7 @@ class DocumentController extends Controller
public function show(Document $doc)
{
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),
]);
}
@ -257,7 +259,7 @@ class DocumentController extends Controller
} else {
$doc->shares()->updateOrCreate(['share_to' => $share['share_to']]);
}
// TODO: plase send email here
Mail::to($share['share_to'])->queue(new DocumentShare($doc));
}
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',
];
protected $cast = [
'start_date' => 'date',
'end_date' => 'date'
protected $casts = [
'start_date' => 'datetime:Y-m-d',
'end_date' => 'datetime:Y-m-d'
];
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/react": "^5.11.2",
"daisyui": "^2.28.0",
"moment": "^2.29.4",
"react-toastify": "^9.0.8",
"react-use": "^17.4.0"
},
@ -1973,6 +1974,14 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"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": {
"version": "2.1.2",
"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",
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

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

@ -6,17 +6,29 @@ import interactionPlugin from "@fullcalendar/interaction" // needed for dayClick
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/inertia-react';
import { Inertia } from '@inertiajs/inertia';
export default function Dashboard(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
// 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
alert(arg.dateStr)
}
}
return (
<AuthenticatedLayout
@ -57,7 +69,7 @@ export default function Dashboard(props) {
plugins={[ dayGridPlugin, interactionPlugin ]}
initialView="dayGridMonth"
dateClick={handleDateClick}
eventClick={(arg) => console.log(arg)}
eventClick={handleEventClick}
events={calenderEvents}
/>
</div>

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

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

@ -71,7 +71,7 @@ export default function ModalShare(props) {
<div className='py-4'>
<div className="flex flex-wrap">
{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'>
<p className='pr-1'>
{share.share_to}

@ -1,3 +1,5 @@
import moment from "moment";
export const statuses = [
{
key: 0,
@ -22,4 +24,8 @@ export const validateEmail = (email) => {
.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,}))$/
);
};
};
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