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()
|
||||
->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')
|
||||
->label('Tentang Kami')
|
||||
->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);
|
||||
}
|
||||
|
||||
public function contacts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Contact::class, 'replied_by');
|
||||
}
|
||||
|
||||
public function news(): HasMany
|
||||
{
|
||||
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