refactor: replace custom HTML infolist with native Filament components for data change requests
This commit is contained in:
parent
a92716cb10
commit
029d92e4a6
@ -35,7 +35,7 @@ protected function setUp(): void
|
||||
$entity = $record->entity;
|
||||
$newData = $record->new_data;
|
||||
|
||||
if (isset($newData['__delete_request__']) && $newData['__delete_request__'] === true) {
|
||||
if (empty($newData)) {
|
||||
$entity->delete();
|
||||
|
||||
$record->update(['status' => DataChangeStatus::APPROVED]);
|
||||
|
||||
@ -4,229 +4,194 @@
|
||||
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry;
|
||||
|
||||
class DataChangeInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
TextEntry::make('comparison')
|
||||
->schema(function (DataChangeRequest $record): array {
|
||||
$old = $record->old_data ?? [];
|
||||
$new = $record->new_data ?? [];
|
||||
|
||||
if (! is_array($old) || ! is_array($new)) {
|
||||
return [
|
||||
TextEntry::make('error')
|
||||
->hiddenLabel()
|
||||
->html()
|
||||
->state(function (DataChangeRequest $record): string {
|
||||
$old = $record->old_data ?? [];
|
||||
$new = $record->new_data ?? [];
|
||||
->state('Format data tidak valid.')
|
||||
->badge()
|
||||
->color('danger'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! is_array($old) || ! is_array($new)) {
|
||||
return self::alert('Format data tidak valid.', 'warning');
|
||||
}
|
||||
if (empty($new)) {
|
||||
return self::buildDeletionView($old);
|
||||
}
|
||||
|
||||
if (isset($new['__delete_request__']) && $new['__delete_request__'] === true) {
|
||||
return self::deletionView($old);
|
||||
}
|
||||
if (empty($old)) {
|
||||
return self::buildAdditionView($new);
|
||||
}
|
||||
|
||||
if (empty($old)) {
|
||||
return self::additionView($new);
|
||||
}
|
||||
|
||||
return self::updateView($old, $new);
|
||||
}),
|
||||
]),
|
||||
])
|
||||
return self::buildUpdateView($old, $new);
|
||||
})
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
private static function deletionView(array $old): string
|
||||
private static function buildDeletionView(array $old): array
|
||||
{
|
||||
$title = CheerfulNotification::getByKey('data_change.deletion_request_title');
|
||||
$desc = CheerfulNotification::getByKey('data_change.deletion_request_desc');
|
||||
|
||||
$rows = collect($old)
|
||||
->map(fn ($v, $k) => self::row($k, self::renderValue($v)))
|
||||
->implode('');
|
||||
|
||||
return self::card(
|
||||
header: self::header('del', $title, $desc, self::badge('Penghapusan', 'del')),
|
||||
body: $rows ?: self::emptyRow(),
|
||||
);
|
||||
return [
|
||||
Section::make($title)
|
||||
->description($desc)
|
||||
->icon('heroicon-o-trash')
|
||||
->schema([
|
||||
TextEntry::make('badge')->hiddenLabel()->state('Penghapusan')->badge()->color('danger'),
|
||||
...collect($old)->map(fn ($v, $k) => self::fieldEntry($k, $v, showLabel: true))->flatten()->toArray(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private static function additionView(array $new): string
|
||||
private static function buildAdditionView(array $new): array
|
||||
{
|
||||
$title = CheerfulNotification::getByKey('data_change.addition_request_title');
|
||||
$desc = CheerfulNotification::getByKey('data_change.addition_request_desc');
|
||||
|
||||
$rows = collect($new)
|
||||
->map(fn ($v, $k) => self::row($k, self::renderValue($v)))
|
||||
->implode('');
|
||||
|
||||
return self::card(
|
||||
header: self::header('add', $title, $desc, self::badge('Penambahan Baru', 'add')),
|
||||
body: $rows ?: self::emptyRow(),
|
||||
);
|
||||
return [
|
||||
Section::make($title)
|
||||
->description($desc)
|
||||
->icon('heroicon-o-plus-circle')
|
||||
->schema([
|
||||
TextEntry::make('badge')->hiddenLabel()->state('Penambahan Baru')->badge()->color('success'),
|
||||
...collect($new)->map(fn ($v, $k) => self::fieldEntry($k, $v, showLabel: true))->flatten()->toArray(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private static function updateView(array $old, array $new): string
|
||||
private static function buildUpdateView(array $old, array $new): array
|
||||
{
|
||||
$changed = collect($new)
|
||||
->map(function ($newVal, $key) use ($old) {
|
||||
$oldVal = $old[$key] ?? null;
|
||||
$title = CheerfulNotification::getByKey('data_change.comparison_title');
|
||||
$desc = CheerfulNotification::getByKey('data_change.comparison_desc');
|
||||
|
||||
if ($oldVal === $newVal) {
|
||||
return null;
|
||||
}
|
||||
$changedFields = collect($new)
|
||||
->filter(fn ($newVal, $key) => ($old[$key] ?? null) !== $newVal);
|
||||
|
||||
return "
|
||||
<div style='display:grid; grid-template-columns:180px 1fr; border-bottom:1px solid var(--dcr-row-border); align-items:stretch;'>
|
||||
<div style='padding:11px 14px; font-size:0.75rem; font-weight:500; color:var(--dcr-key-color); letter-spacing:0.025em; background:var(--dcr-key-bg); border-right:1px solid var(--dcr-key-border); display:flex; align-items:center; word-break:break-word;'>".e($key)."</div>
|
||||
<div style='display:grid; grid-template-columns:1fr 1fr;'>
|
||||
<div style='padding:11px 14px; background:var(--dcr-old-bg); border-right:1px solid var(--dcr-old-border); color:var(--dcr-old-color); word-break:break-word; font-size:0.8125rem;'>".self::renderValue($oldVal)."</div>
|
||||
<div style='padding:11px 14px; background:var(--dcr-new-bg); color:var(--dcr-new-color); word-break:break-word; font-size:0.8125rem;'>".self::renderValue($newVal).'</div>
|
||||
</div>
|
||||
</div>';
|
||||
})
|
||||
->filter();
|
||||
|
||||
if ($changed->isEmpty()) {
|
||||
return self::alert('Tidak ada perubahan data yang terdeteksi.', 'info');
|
||||
if ($changedFields->isEmpty()) {
|
||||
return [
|
||||
TextEntry::make('info')->hiddenLabel()->state('Tidak ada perubahan data terdeteksi.')->badge()->color('info'),
|
||||
];
|
||||
}
|
||||
|
||||
$count = $changed->count();
|
||||
$label = $count === 1 ? '1 field diubah' : "{$count} field diubah";
|
||||
$fieldCount = $changedFields->count();
|
||||
$label = $fieldCount === 1 ? '1 field diubah' : "{$fieldCount} field diubah";
|
||||
|
||||
$diffHeader = "
|
||||
<div style='display:grid; grid-template-columns:180px 1fr;'>
|
||||
<div style='padding:8px 14px; background:var(--dcr-key-bg); border-right:1px solid var(--dcr-key-border); border-bottom:1px solid var(--dcr-header-border);'></div>
|
||||
<div style='display:grid; grid-template-columns:1fr 1fr; border-bottom:1px solid var(--dcr-header-border);'>
|
||||
<div style='padding:8px 14px; font-size:0.7rem; font-weight:600; color:var(--dcr-key-color); text-transform:uppercase; letter-spacing:0.06em; background:var(--dcr-old-bg); border-right:1px solid var(--dcr-old-border);'>Sebelum</div>
|
||||
<div style='padding:8px 14px; font-size:0.7rem; font-weight:600; color:var(--dcr-key-color); text-transform:uppercase; letter-spacing:0.06em; background:var(--dcr-new-bg);'>Sesudah</div>
|
||||
</div>
|
||||
</div>";
|
||||
return [
|
||||
Section::make($title)
|
||||
->description($desc)
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->schema([
|
||||
TextEntry::make('badge')->hiddenLabel()->state($label)->badge()->color('primary'),
|
||||
Grid::make(1)
|
||||
->schema(
|
||||
$changedFields->map(function ($newVal, $key) use ($old) {
|
||||
$oldVal = $old[$key] ?? null;
|
||||
|
||||
return self::card(
|
||||
header: self::header('edit', CheerfulNotification::getByKey('data_change.comparison_title'), CheerfulNotification::getByKey('data_change.comparison_desc'), self::badge($label, 'default')),
|
||||
body: $diffHeader.$changed->implode(''),
|
||||
);
|
||||
return [
|
||||
Section::make($key)
|
||||
->columnSpanFull()
|
||||
->compact()
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Grid::make(1)
|
||||
->schema([
|
||||
TextEntry::make("{$key}_old_label")
|
||||
->hiddenLabel()
|
||||
->state('SEBELUM')
|
||||
->weight('bold')
|
||||
->size('xs')
|
||||
->color('danger'),
|
||||
...self::fieldEntry("{$key}_old", $oldVal, showLabel: false),
|
||||
])->columnSpan(1),
|
||||
|
||||
Grid::make(1)
|
||||
->schema([
|
||||
TextEntry::make("{$key}_new_label")
|
||||
->hiddenLabel()
|
||||
->state('SESUDAH')
|
||||
->weight('bold')
|
||||
->size('xs')
|
||||
->color('success'),
|
||||
...self::fieldEntry("{$key}_new", $newVal, showLabel: false),
|
||||
])->columnSpan(1),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
})->flatten()->toArray()
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private static function card(string $header, string $body): string
|
||||
private static function fieldEntry(string $label, mixed $value, bool $showLabel = true): array
|
||||
{
|
||||
return "
|
||||
<div style='border:1px solid var(--dcr-border); border-radius:var(--c-border-radius); overflow:hidden; background:var(--dcr-bg); font-family:var(--font-family,Inter,ui-sans-serif,sans-serif); font-size:0.8125rem;'>
|
||||
{$header}
|
||||
<div>{$body}</div>
|
||||
</div>";
|
||||
}
|
||||
$id = str($label)->slug('_')->toString();
|
||||
|
||||
private static function header(string $type, string $title, string $desc, string $badge): string
|
||||
{
|
||||
$icon = self::icon($type);
|
||||
|
||||
return "
|
||||
<div style='padding:14px 18px; background:var(--dcr-header-bg); border-bottom:1px solid var(--dcr-header-border); display:flex; align-items:center; gap:10px;'>
|
||||
{$icon}
|
||||
<div style='flex:1; min-width:0;'>
|
||||
<div style='font-size:0.875rem; font-weight:600; color:var(--dcr-title); line-height:1.4;'>{$title}</div>
|
||||
<div style='font-size:0.75rem; color:var(--dcr-desc); margin-top:1px;'>{$desc}</div>
|
||||
</div>
|
||||
{$badge}
|
||||
</div>";
|
||||
}
|
||||
|
||||
private static function row(string $key, string $value): string
|
||||
{
|
||||
return "
|
||||
<div style='display:grid; grid-template-columns:180px 1fr; border-bottom:1px solid var(--dcr-row-border);'>
|
||||
<div style='padding:11px 14px; font-size:0.75rem; font-weight:500; color:var(--dcr-key-color); letter-spacing:0.025em; background:var(--dcr-key-bg); border-right:1px solid var(--dcr-key-border); display:flex; align-items:flex-start; word-break:break-word;'>".e($key)."</div>
|
||||
<div style='padding:11px 14px; color:var(--dcr-val-color); word-break:break-word;'>{$value}</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private static function emptyRow(): string
|
||||
{
|
||||
return "<div style='padding:18px; font-size:0.8125rem; color:var(--dcr-muted); font-style:italic;'>Tidak ada data tersedia.</div>";
|
||||
}
|
||||
|
||||
private static function badge(string $label, string $variant): string
|
||||
{
|
||||
[$bg, $color, $border] = match ($variant) {
|
||||
'del' => ['var(--dcr-badge-del-bg)', 'var(--dcr-badge-del-color)', 'var(--dcr-badge-del-border)'],
|
||||
'add' => ['var(--dcr-badge-add-bg)', 'var(--dcr-badge-add-color)', 'var(--dcr-badge-add-border)'],
|
||||
default => ['var(--dcr-badge-bg)', 'var(--dcr-badge-color)', 'var(--dcr-badge-border)'],
|
||||
};
|
||||
|
||||
return "<span style='display:inline-flex; align-items:center; border-radius:var(--c-border-radius); padding:2px 8px; font-size:0.7rem; font-weight:600; letter-spacing:0.025em; white-space:nowrap; background:{$bg}; color:{$color}; border:1px solid {$border};'>{$label}</span>";
|
||||
}
|
||||
|
||||
private static function alert(string $message, string $type = 'info'): string
|
||||
{
|
||||
$icon = $type === 'warning' ? '⚠' : 'ℹ';
|
||||
|
||||
return "
|
||||
<div style='background:var(--dcr-alert-bg); border:1px solid var(--dcr-alert-border); border-radius:var(--c-border-radius); padding:14px 16px; display:flex; align-items:flex-start; gap:10px; font-family:var(--font-family,Inter,ui-sans-serif,sans-serif);'>
|
||||
<span style='color:var(--dcr-alert-color); font-size:0.875rem; margin-top:1px; flex-shrink:0;'>{$icon}</span>
|
||||
<span style='color:var(--dcr-alert-color); font-size:0.8125rem; font-weight:500;'>{$message}</span>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private static function icon(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'del' => "<svg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='var(--dcr-icon-del)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' style='flex-shrink:0'><polyline points='3 6 5 6 21 6'/><path d='M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6'/><path d='M10 11v6M14 11v6'/><path d='M9 6V4h6v2'/></svg>",
|
||||
'add' => "<svg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='var(--dcr-icon-add)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' style='flex-shrink:0'><line x1='12' y1='5' x2='12' y2='19'/><line x1='5' y1='12' x2='19' y2='12'/></svg>",
|
||||
'edit' => "<svg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='var(--dcr-icon-edit)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' style='flex-shrink:0'><path d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/><path d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z'/></svg>",
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private static function renderValue(mixed $value): string
|
||||
{
|
||||
if ($value === null || $value === '' || (is_array($value) && empty($value))) {
|
||||
return "<span style='color:var(--dcr-muted); font-size:0.8rem;'>—</span>";
|
||||
return [
|
||||
TextEntry::make($id)
|
||||
->label($label)
|
||||
->hiddenLabel(! $showLabel)
|
||||
->state('—')
|
||||
->color('gray'),
|
||||
];
|
||||
}
|
||||
|
||||
$paths = is_array($value) ? $value : [$value];
|
||||
$components = [];
|
||||
|
||||
return collect($paths)->map(function ($item) {
|
||||
if (! is_string($item)) {
|
||||
return "<span style='color:var(--dcr-val-color); font-size:0.8125rem;'>".e(json_encode($item)).'</span>';
|
||||
}
|
||||
foreach ($paths as $index => $item) {
|
||||
$uniqueId = "{$id}_{$index}";
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $item)) {
|
||||
return "<a href='{$item}' target='_blank' style='color:var(--dcr-link); text-decoration:underline; font-size:0.8125rem; word-break:break-all;'>".e($item).'</a>';
|
||||
}
|
||||
|
||||
if ((str_contains($item, '/') || str_contains($item, '\\')) && ! str_contains($item, ' ')) {
|
||||
$url = Storage::disk(config('filesystems.default'))->url($item);
|
||||
if (is_string($item) && (str_contains($item, '/') || str_contains($item, '\\'))) {
|
||||
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) {
|
||||
return "<img src='{$url}' style='max-height:96px; border-radius:var(--c-border-radius); border:1px solid var(--dcr-img-border); margin-top:4px; display:block;'>";
|
||||
$components[] = ImageEntry::make($uniqueId)
|
||||
->label($index === 0 ? $label : '')
|
||||
->hiddenLabel(! ($showLabel && $index === 0))
|
||||
->disk(config('filesystems.default'))
|
||||
->state($item)
|
||||
->extraImgAttributes(['style' => 'max-height: 200px; width: auto; border-radius: 0.5rem;']);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ext === 'pdf') {
|
||||
return "
|
||||
<div style='margin-top:6px;'>
|
||||
<iframe src='{$url}' style='width:100%; height:280px; border:1px solid var(--dcr-img-border); border-radius:var(--c-border-radius);'></iframe>
|
||||
</div>";
|
||||
$components[] = PdfViewerEntry::make($uniqueId)
|
||||
->label($index === 0 ? $label : '')
|
||||
->hiddenLabel(! ($showLabel && $index === 0))
|
||||
->minHeight('500px')
|
||||
->state(Storage::disk(config('filesystems.default'))->url($item));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$extUp = $ext ? strtoupper($ext) : 'FILE';
|
||||
|
||||
return "<a href='{$url}' style='display:inline-flex; align-items:center; gap:4px; color:var(--dcr-link); font-size:0.8rem; font-weight:500; text-decoration:none;'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48'/></svg>
|
||||
Dokumen {$extUp}
|
||||
</a>";
|
||||
}
|
||||
|
||||
return "<span style='color:var(--dcr-val-color); font-size:0.8125rem;'>".e($item).'</span>';
|
||||
})->implode('<br>');
|
||||
$components[] = TextEntry::make($uniqueId)
|
||||
->label($index === 0 ? $label : '')
|
||||
->hiddenLabel(! ($showLabel && $index === 0))
|
||||
->state(is_scalar($item) ? $item : json_encode($item));
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,9 +28,10 @@ protected function setUp(): void
|
||||
|
||||
$this->successNotification(null);
|
||||
|
||||
$this->modalHeading(fn () => auth()->user()->company?->verificationRequest?->status === VerificationStatus::APPROVED
|
||||
? CheerfulNotification::getByKey('journalist.request_deletion_title')
|
||||
: CheerfulNotification::getByKey('journalist.delete_title')
|
||||
$this->modalHeading(
|
||||
fn () => auth()->user()->company?->verificationRequest?->status === VerificationStatus::APPROVED
|
||||
? CheerfulNotification::getByKey('journalist.request_deletion_title')
|
||||
: CheerfulNotification::getByKey('journalist.delete_title')
|
||||
);
|
||||
|
||||
$this->schema(fn () => auth()->user()->company?->verificationRequest?->status === VerificationStatus::APPROVED ? [
|
||||
@ -58,12 +59,42 @@ protected function setUp(): void
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldLabels = Journalist::dataChangeEntryLabels();
|
||||
$oldFields = [];
|
||||
|
||||
// Standard fields
|
||||
foreach (['name', 'email', 'phone_number', 'press_card', 'ukw_certificate'] as $f) {
|
||||
$label = $fieldLabels[$f] ?? $f;
|
||||
$val = $record->{$f};
|
||||
|
||||
// Resolve enums if necessary
|
||||
if ($val instanceof \BackedEnum) {
|
||||
$val = $val->value;
|
||||
}
|
||||
|
||||
$oldFields[$label] = $val ?? '-';
|
||||
}
|
||||
|
||||
// Document fields
|
||||
$mediaItems = $record->getMedia('journalists');
|
||||
foreach (['press_card_docs', 'ukw_certificate_docs'] as $df) {
|
||||
$label = $fieldLabels[$df] ?? $df;
|
||||
$docType = str($df)->replace('_docs', '')->slug('-');
|
||||
|
||||
$currentMedia = $mediaItems
|
||||
->where('custom_properties.doc_type', $docType)
|
||||
->sortByDesc('created_at')
|
||||
->first();
|
||||
|
||||
$oldFields[$label] = $currentMedia ? [$currentMedia->getPathRelativeToRoot()] : [];
|
||||
}
|
||||
|
||||
$dataChangeRequest = DataChangeRequest::create([
|
||||
'user_id' => $user->id,
|
||||
'entity_type' => Journalist::class,
|
||||
'entity_id' => $record->id,
|
||||
'old_data' => $record->toArray(),
|
||||
'new_data' => ['__delete_request__' => true],
|
||||
'old_data' => $oldFields,
|
||||
'new_data' => [],
|
||||
'change_reason' => $data['deletion_reason'] ?? 'Penghapusan jurnalis',
|
||||
]);
|
||||
|
||||
|
||||
@ -708,6 +708,14 @@
|
||||
'cheerful' => 'Maaf ya, pengajuan perubahan data kamu terpaksa ditolak untuk saat ini. Tetap semangat! 💪',
|
||||
'formal' => 'Pengajuan perubahan data Anda tidak dapat disetujui.',
|
||||
],
|
||||
'deletion_approved' => [
|
||||
'cheerful' => 'Hapus Berhasil! 🗑️🚀',
|
||||
'formal' => 'Penghapusan Disetujui',
|
||||
],
|
||||
'deletion_success_desc' => [
|
||||
'cheerful' => 'Sip! Data tersebut sudah berhasil dihapus secara permanen dari sistem sebagaimana permintaanmu. ✨🚮',
|
||||
'formal' => 'Data tersebut telah berhasil dihapus dari sistem sesuai permohonan.',
|
||||
],
|
||||
],
|
||||
'visitor' => [
|
||||
'empty_state' => [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user