feat: Implement contact management system with email reply functionality and database migration
This commit is contained in:
parent
0382570c23
commit
3ae2054ebd
@ -54,6 +54,19 @@ public function form(Schema $schema): Schema
|
|||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
|
||||||
|
TextInput::make('site_email')
|
||||||
|
->label('Email Website')
|
||||||
|
->placeholder('diskominfo@purwakartakab.go.id')
|
||||||
|
->email()
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
|
||||||
|
TextInput::make('site_phone')
|
||||||
|
->label('Nomor Telepon')
|
||||||
|
->placeholder('(0264) 200222')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
|
||||||
RichEditor::make('about_us')
|
RichEditor::make('about_us')
|
||||||
->label('Tentang Kami')
|
->label('Tentang Kami')
|
||||||
->placeholder('...')
|
->placeholder('...')
|
||||||
|
|||||||
199
app/Filament/Resources/Manage/Contacts/ContactResource.php
Normal file
199
app/Filament/Resources/Manage/Contacts/ContactResource.php
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Manage\Contacts;
|
||||||
|
|
||||||
|
use App\Filament\Actions\DefaultBulkActions;
|
||||||
|
use App\Filament\Resources\Manage\Contacts\Pages\ManageContacts;
|
||||||
|
use App\Mail\ContactReplyMail;
|
||||||
|
use App\Models\Contact;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Infolists\Components\TextEntry;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class ContactResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Contact::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::Envelope;
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Kontak';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'subject';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Kontak';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'manage/contacts';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::where('replied_at', null)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
// Form usually not needed as we use View/Reply
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function infolist(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->schema([
|
||||||
|
Section::make('Informasi Pengirim')
|
||||||
|
->schema([
|
||||||
|
Grid::make(4)
|
||||||
|
->schema([
|
||||||
|
TextEntry::make('name')
|
||||||
|
->label('Nama'),
|
||||||
|
|
||||||
|
TextEntry::make('email')
|
||||||
|
->label('Email'),
|
||||||
|
|
||||||
|
TextEntry::make('subject')
|
||||||
|
->label('Subyek'),
|
||||||
|
|
||||||
|
TextEntry::make('created_at')
|
||||||
|
->label('Diterima pada')
|
||||||
|
->date('l, d F Y H:i'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Section::make('Pesan')
|
||||||
|
->schema([
|
||||||
|
TextEntry::make('message')
|
||||||
|
->label('Isi Pesan')
|
||||||
|
->columnSpanFull(),
|
||||||
|
])
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Section::make('Balasan')
|
||||||
|
->schema([
|
||||||
|
TextEntry::make('repliedBy.name')
|
||||||
|
->label('Dibalas Oleh')
|
||||||
|
->placeholder('Belum dibalas'),
|
||||||
|
|
||||||
|
TextEntry::make('replied_at')
|
||||||
|
->label('Dibalas pada')
|
||||||
|
->date('l, d F Y H:i')
|
||||||
|
->placeholder('Belum dibalas'),
|
||||||
|
|
||||||
|
TextEntry::make('reply_message')
|
||||||
|
->label('Pesan Balasan')
|
||||||
|
->placeholder('Belum dibalas'),
|
||||||
|
])
|
||||||
|
->columnSpanFull()
|
||||||
|
->visible(fn ($record) => $record->replied_at !== null),
|
||||||
|
])
|
||||||
|
->columns(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Nama')
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
|
TextColumn::make('email')
|
||||||
|
->label('Email')
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
|
TextColumn::make('subject')
|
||||||
|
->label('Subyek')
|
||||||
|
->limit(30),
|
||||||
|
|
||||||
|
IconColumn::make('replied_at')
|
||||||
|
->label('Dibalas')
|
||||||
|
->boolean()
|
||||||
|
->getStateUsing(fn ($record) => $record->replied_at !== null),
|
||||||
|
|
||||||
|
TextColumn::make('repliedBy.name')
|
||||||
|
->label('Dibalas Oleh')
|
||||||
|
->badge()
|
||||||
|
->placeholder('Belum dibalas')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Diterima pada')
|
||||||
|
->date('l, d F Y H:i')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
ViewAction::make()
|
||||||
|
->label('Lihat'),
|
||||||
|
|
||||||
|
Action::make('reply')
|
||||||
|
->label('Balas')
|
||||||
|
->icon(Heroicon::ChatBubbleLeftRight)
|
||||||
|
->color('success')
|
||||||
|
->modalHeading('Balas Pesan Kontak')
|
||||||
|
->modalDescription('Pesan balasan tidak bisa diubah setelah dikirim.')
|
||||||
|
->schema([
|
||||||
|
Textarea::make('reply_message')
|
||||||
|
->label('Pesan Balasan')
|
||||||
|
->placeholder(fn ($record) => "Halo {$record->name}, ...")
|
||||||
|
->required()
|
||||||
|
->rows(5),
|
||||||
|
])
|
||||||
|
->action(function (Contact $record, array $data) {
|
||||||
|
Mail::to($record->email)->send(new ContactReplyMail($record, $data['reply_message']));
|
||||||
|
|
||||||
|
$record->update([
|
||||||
|
'reply_message' => $data['reply_message'],
|
||||||
|
'replied_at' => now(),
|
||||||
|
'replied_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Balasan terkirim')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
})
|
||||||
|
->visible(fn ($record) => $record->replied_at === null),
|
||||||
|
|
||||||
|
DeleteAction::make()
|
||||||
|
->label('Hapus'),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
...DefaultBulkActions::make('Kontak'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateIcon(Heroicon::Envelope)
|
||||||
|
->emptyStateDescription('Belum ada kontak atau pesan masuk.')
|
||||||
|
->defaultSort('created_at', 'desc')
|
||||||
|
->deferFilters(false)
|
||||||
|
->reorderable('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ManageContacts::route('/'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Manage\Contacts\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Manage\Contacts\ContactResource;
|
||||||
|
use Filament\Resources\Pages\ManageRecords;
|
||||||
|
|
||||||
|
class ManageContacts extends ManageRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = ContactResource::class;
|
||||||
|
}
|
||||||
64
app/Mail/ContactReplyMail.php
Normal file
64
app/Mail/ContactReplyMail.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ContactReplyMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public $contact,
|
||||||
|
public $replyMessage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*/
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Balasan: '.($this->contact->subject ?? 'Pesan Kontak'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*/
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
$settings = app(GeneralSettings::class);
|
||||||
|
$logoPath = $settings->site_logo
|
||||||
|
? storage_path('app/public/'.$settings->site_logo)
|
||||||
|
: public_path('assets/img/logo/icon-with-text.png');
|
||||||
|
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.contact-reply',
|
||||||
|
with: [
|
||||||
|
'name' => $this->contact->name,
|
||||||
|
'replyMessage' => $this->replyMessage,
|
||||||
|
'site_name' => $settings->site_name,
|
||||||
|
'logoPath' => $logoPath,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Models/Contact.php
Normal file
24
app/Models/Contact.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Contact extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'subject',
|
||||||
|
'message',
|
||||||
|
'reply_message',
|
||||||
|
'replied_at',
|
||||||
|
'replied_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function repliedBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'replied_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,6 +57,11 @@ public function company(): HasOne
|
|||||||
return $this->hasOne(Company::class);
|
return $this->hasOne(Company::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contacts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Contact::class, 'replied_by');
|
||||||
|
}
|
||||||
|
|
||||||
public function news(): HasMany
|
public function news(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(News::class, 'author_id');
|
return $this->hasMany(News::class, 'author_id');
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
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('contacts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('email', 254);
|
||||||
|
$table->string('subject')->nullable();
|
||||||
|
$table->text('message');
|
||||||
|
$table->text('reply_message')->nullable();
|
||||||
|
$table->timestamp('replied_at')->nullable();
|
||||||
|
$table->foreignIdFor(User::class, 'replied_by')->nullable()->constrained()->cascadeOnDelete();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('contacts');
|
||||||
|
}
|
||||||
|
};
|
||||||
116
resources/views/emails/contact-reply.blade.php
Normal file
116
resources/views/emails/contact-reply.blade.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:400,600" rel="stylesheet" type="text/css">
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
font-family: 'Roboto', sans-serif !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #8094ae;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
mso-table-lspace: 0pt !important;
|
||||||
|
mso-table-rspace: 0pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
table-layout: fixed !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table table table {
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body width="100%"
|
||||||
|
style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f6fa;">
|
||||||
|
<center style="width: 100%; background-color: #f5f6fa;">
|
||||||
|
<table width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#f5f6fa">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 0;">
|
||||||
|
<table style="width:100%;max-width:620px;margin:0 auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding-bottom:25px;">
|
||||||
|
<a href="{{ url('/') }}">
|
||||||
|
<img src="{{ $message->embed($logoPath) }}" alt="{{ $site_name }}"
|
||||||
|
style="height:40px; display:block; margin:0 auto;">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p style="font-size:14px; padding-top:12px; text-align:center;">
|
||||||
|
Sistem Informasi Media Komunikasi.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table style="width:100%;max-width:620px;margin:0 auto;background-color:#ffffff;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px 30px 20px">
|
||||||
|
<p style="margin-bottom: 10px;">Hello {{ $name }},</p>
|
||||||
|
<p style="margin-bottom: 10px;">{!! nl2br(e($replyMessage)) !!}</p>
|
||||||
|
<p style="margin-top: 25px; margin-bottom: 15px;">---- <br> Regards <br>
|
||||||
|
{{ $site_name }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table style="width:100%;max-width:620px;margin:0 auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; padding:25px 20px 0;">
|
||||||
|
<p style="font-size: 13px;">© {{ date('Y') }}
|
||||||
|
{{ $site_name }}. Seluruh Hak Cipta
|
||||||
|
Dilindungi.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user