Compare commits

...

10 Commits

13 changed files with 323 additions and 66 deletions

View File

@ -37,6 +37,7 @@ public function handle()
'migrate:company' => '🏭 Migrating companies & partner media...', 'migrate:company' => '🏭 Migrating companies & partner media...',
'migrate:journalist' => '📰 Migrating journalists...', 'migrate:journalist' => '📰 Migrating journalists...',
'migrate:media-orders' => '📦 Migrating media orders...', 'migrate:media-orders' => '📦 Migrating media orders...',
'migrate:reports' => '📋 Migrating report proofs (bukti tayang)...',
'migrate:media-cooperation' => '🤝 Migrating media cooperation...', 'migrate:media-cooperation' => '🤝 Migrating media cooperation...',
'migrate:media-monitoring' => '🖥️ Migrating media monitoring...', 'migrate:media-monitoring' => '🖥️ Migrating media monitoring...',
'migrate:issue-management' => '⚠️ Migrating issue management...', 'migrate:issue-management' => '⚠️ Migrating issue management...',

View File

@ -381,6 +381,24 @@ private function processReport(object $legacyReport, int $cooperationId, int $ta
[$cleanTitle, $cleanLink] = $this->extractLinkData($legacyReport); [$cleanTitle, $cleanLink] = $this->extractLinkData($legacyReport);
$existingReportId = DB::table('reports')
->where('media_task_assignment_id', $mediaTaskAssignment->id)
->where('publication_date', $legacyReport->broadcast)
->where('link', $cleanLink)
->value('id');
if ($existingReportId) {
if (! isset($this->migratedReportIds[$existingReportId])) {
$report = Report::find($existingReportId);
if ($report) {
$this->migrateProof($report, $legacyReport);
$this->migratedReportIds[$existingReportId] = true;
}
}
return;
}
DB::table('reports')->insert([ DB::table('reports')->insert([
'media_task_assignment_id' => $mediaTaskAssignment->id, 'media_task_assignment_id' => $mediaTaskAssignment->id,
'title' => $cleanTitle, 'title' => $cleanTitle,
@ -394,13 +412,13 @@ private function processReport(object $legacyReport, int $cooperationId, int $ta
$newReportId = (int) DB::getPdo()->lastInsertId(); $newReportId = (int) DB::getPdo()->lastInsertId();
// // Migrate proof image if not already done if (! isset($this->migratedReportIds[$newReportId])) {
// if (! isset($this->migratedReportIds[$newReportId])) { $report = Report::find($newReportId);
// $report = Report::find($newReportId); if ($report) {
// if ($report) { $this->migrateProof($report, $legacyReport);
// $this->migrateProof($report, $legacyReport); $this->migratedReportIds[$newReportId] = true;
// } }
// } }
} }
/** /**

View File

@ -134,12 +134,26 @@ private function processReport(object $legacy): void
return; return;
} }
// Insert the report [$cleanTitle, $cleanLink] = $this->extractLinkData($legacy);
$existing = DB::table('reports')
->where('media_task_assignment_id', $mediaTaskAssignment->id)
->where('publication_date', $legacy->broadcast)
->where('link', $cleanLink)
->first();
if ($existing) {
$proofMigrated = $this->migrateProofIfNeeded($existing->id, $legacy);
$this->output->writeln($proofMigrated ? '<info>SKIP + IMG</info>' : '<info>SKIP</info>');
return;
}
DB::table('reports')->insert([ DB::table('reports')->insert([
'media_task_assignment_id' => $mediaTaskAssignment->id, 'media_task_assignment_id' => $mediaTaskAssignment->id,
'title' => $legacy->title ?? '', 'title' => $cleanTitle,
'publication_date' => $legacy->broadcast, 'publication_date' => $legacy->broadcast,
'link' => $legacy->link ?? '', 'link' => $cleanLink,
'description' => '', 'description' => '',
'status' => $this->mapApprovalStatus((string) $legacy->status), 'status' => $this->mapApprovalStatus((string) $legacy->status),
'created_at' => $legacy->created_at ?? now(), 'created_at' => $legacy->created_at ?? now(),
@ -148,32 +162,46 @@ private function processReport(object $legacy): void
$newReportId = (int) DB::getPdo()->lastInsertId(); $newReportId = (int) DB::getPdo()->lastInsertId();
// Migrate proof image if not already done $proofMigrated = $this->migrateProofIfNeeded($newReportId, $legacy);
if (! isset($this->migratedIds[$newReportId])) {
$report = Report::find($newReportId);
if ($report) {
$this->migrateProof($report, $legacy);
}
}
$this->output->writeln('<info>DONE</info>'); $this->output->writeln($proofMigrated ? '<info>DONE + IMG</info>' : '<info>DONE</info>');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>'); $this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
} }
} }
/** /**
* Migrate the proof image file from S3 to the Report media library. * Register proof media for a report when not yet migrated.
*/ */
private function migrateProof(Report $report, object $legacy): void private function migrateProofIfNeeded(int $reportId, object $legacy): bool
{
if (isset($this->migratedIds[$reportId])) {
return false;
}
$report = Report::find($reportId);
if (! $report) {
return false;
}
return $this->migrateProof($report, $legacy);
}
/**
* Migrate the proof image metadata (file stays on S3 legacy path).
*/
private function migrateProof(Report $report, object $legacy): bool
{ {
$legacyPath = trim($legacy->proof ?? ''); $legacyPath = trim($legacy->proof ?? '');
if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') { if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
return; return false;
} }
$sourcePath = 'old-data/reports/'.$legacyPath; $sourcePath = 'old-data/reports/'.$legacyPath;
$this->importProofFile($report, $sourcePath, $legacy); $this->importProofFile($report, $sourcePath, $legacy);
$this->migratedIds[$report->id] = true;
return true;
} }
/** /**
@ -222,6 +250,45 @@ private function guessMime(string $fileName): string
}; };
} }
/**
* Ekstrak title & link bersih dari legacy report.
*
* @return array{0: string, 1: string} [title, link]
*/
private function extractLinkData(object $legacyReport): array
{
$rawTitle = trim($legacyReport->title ?? '');
$rawLink = trim($legacyReport->link ?? '');
$titleIsUrl = (bool) preg_match('#^https?://#i', $rawTitle);
preg_match('#https?://\S+#', $rawLink, $urlMatches);
$extractedUrl = rtrim($urlMatches[0] ?? '', '.,;)');
if ($titleIsUrl) {
$cleanLink = $rawTitle;
$lines = array_values(array_filter(
array_map('trim', preg_split('/[\r\n]+/', $rawLink))
));
$candidateTitle = '';
foreach ($lines as $line) {
if (! preg_match('#^https?://#i', $line)) {
$candidateTitle = $line;
break;
}
}
$cleanTitle = $candidateTitle !== ''
? mb_substr($candidateTitle, 0, 255)
: mb_substr($rawTitle, 0, 255);
} else {
$cleanLink = $extractedUrl ?: mb_substr($rawLink, 0, 2048);
$cleanTitle = mb_substr($rawTitle, 0, 255);
}
return [$cleanTitle, $cleanLink];
}
/** /**
* Map legacy reports.status integer to ApprovalStatus enum value. * Map legacy reports.status integer to ApprovalStatus enum value.
* Legacy: 0 = menunggu, 1 = menerima, 2 = menolak * Legacy: 0 = menunggu, 1 = menerima, 2 = menolak

View File

@ -156,7 +156,8 @@ public function form(Schema $schema): Schema
'indigo' => '🌌 Indigo (Ungu Gelap)', 'indigo' => '🌌 Indigo (Ungu Gelap)',
]) ])
->placeholder('Pilih warna tema') ->placeholder('Pilih warna tema')
->native(false), ->native(false)
->required(),
Select::make('settings.font') Select::make('settings.font')
->label('Jenis Huruf (Font)') ->label('Jenis Huruf (Font)')
@ -168,7 +169,8 @@ public function form(Schema $schema): Schema
'Montserrat' => 'Montserrat (Classic)', 'Montserrat' => 'Montserrat (Classic)',
'Lexend' => 'Lexend (Readable)', 'Lexend' => 'Lexend (Readable)',
]) ])
->native(false), ->native(false)
->required(),
Select::make('settings.content_width') Select::make('settings.content_width')
->label('Lebar Konten') ->label('Lebar Konten')
@ -176,7 +178,8 @@ public function form(Schema $schema): Schema
'full' => '↔️ Lebar Penuh (Full)', 'full' => '↔️ Lebar Penuh (Full)',
'centered' => '🏢 Terpusat (Centered)', 'centered' => '🏢 Terpusat (Centered)',
]) ])
->native(false), ->native(false)
->required(),
Select::make('settings.border_radius') Select::make('settings.border_radius')
->label('Radius Sudut (Border)') ->label('Radius Sudut (Border)')
@ -186,7 +189,8 @@ public function form(Schema $schema): Schema
'lg' => '🎉 Rounded (Cheerful)', 'lg' => '🎉 Rounded (Cheerful)',
'xl' => '🎈 Extra Round', 'xl' => '🎈 Extra Round',
]) ])
->native(false), ->native(false)
->required(),
Toggle::make('settings.top_navigation') Toggle::make('settings.top_navigation')
->label('Navigasi Atas (Top Nav)') ->label('Navigasi Atas (Top Nav)')

View File

@ -3,14 +3,18 @@
namespace App\Filament\Resources\Manage\Cooperations\Actions\Report; namespace App\Filament\Resources\Manage\Cooperations\Actions\Report;
use App\Enums\RoleEnum; use App\Enums\RoleEnum;
use App\Filament\Support\MediaDataUri;
use App\Settings\GeneralSettings; use App\Settings\GeneralSettings;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
class ExportAllReportsPdfAction extends Action class ExportAllReportsPdfAction extends Action
{ {
private const MAX_RECORDS = 500;
public static function getDefaultName(): ?string public static function getDefaultName(): ?string
{ {
return 'export_all_pdf'; return 'export_all_pdf';
@ -24,29 +28,43 @@ protected function setUp(): void
->color('info') ->color('info')
->icon('heroicon-o-arrow-down-tray') ->icon('heroicon-o-arrow-down-tray')
->action(function (Table $table) { ->action(function (Table $table) {
$settings = app(GeneralSettings::class); $query = $table->getLivewire()->getFilteredTableQuery();
$records = $table->getLivewire()->getFilteredTableQuery()->get(); $count = (clone $query)->count();
$logoPath = $settings->site_icon ? storage_path('app/public/'.$settings->site_icon) : null; if ($count > self::MAX_RECORDS) {
$logoBase64 = null; Notification::make()
if ($logoPath && file_exists($logoPath)) { ->title('Data terlalu banyak')
$logoData = base64_encode(file_get_contents($logoPath)); ->body('Maksimal '.self::MAX_RECORDS.' laporan per unduhan. Gunakan filter tabel untuk mempersempit data.')
$logoBase64 = 'data:image/'.pathinfo($logoPath, PATHINFO_EXTENSION).';base64,'.$logoData; ->warning()
->send();
return;
} }
$records->each(function ($record) { $settings = app(GeneralSettings::class);
$image = $record->getFirstMedia('reports');
$record->imageBase64 = null; $records = $query
if ($image) { ->with([
$imagePath = $image->getPath(); 'mediaTaskAssignment.partnerMedia',
if (file_exists($imagePath)) { 'media',
$imageData = base64_encode(file_get_contents($imagePath)); ])
$record->imageBase64 = 'data:image/'.pathinfo($imagePath, PATHINFO_EXTENSION).';base64,'.$imageData; ->get();
}
} $logoPath = $settings->site_icon ? storage_path('app/public/'.$settings->site_icon) : null;
}); $logoBase64 = MediaDataUri::fromPath($logoPath, maxWidth: 60, maxHeight: 60);
foreach ($records as $record) {
$record->imageBase64 = MediaDataUri::thumbnailForPdf(
$record->getFirstMedia('reports'),
maxWidth: 150,
maxHeight: 80,
);
}
return response()->streamDownload(function () use ($records, $settings, $logoBase64) { return response()->streamDownload(function () use ($records, $settings, $logoBase64) {
@ini_set('memory_limit', '512M');
set_time_limit(120);
echo Pdf::loadHtml( echo Pdf::loadHtml(
Blade::render('filament.manage.reports.print-all', [ Blade::render('filament.manage.reports.print-all', [
'records' => $records, 'records' => $records,

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\Manage\Cooperations\Actions\Report; namespace App\Filament\Resources\Manage\Cooperations\Actions\Report;
use App\Enums\RoleEnum; use App\Enums\RoleEnum;
use App\Filament\Support\MediaDataUri;
use App\Models\Report; use App\Models\Report;
use App\Settings\GeneralSettings; use App\Settings\GeneralSettings;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
@ -27,24 +28,17 @@ protected function setUp(): void
$settings = app(GeneralSettings::class); $settings = app(GeneralSettings::class);
$logoPath = $settings->site_icon ? storage_path('app/public/'.$settings->site_icon) : null; $logoPath = $settings->site_icon ? storage_path('app/public/'.$settings->site_icon) : null;
$logoBase64 = MediaDataUri::fromPath($logoPath, maxWidth: 80, maxHeight: 100);
$logoBase64 = null; $imageBase64 = MediaDataUri::thumbnailForPdf(
if ($logoPath && file_exists($logoPath)) { $record->getFirstMedia('reports'),
$logoData = base64_encode(file_get_contents($logoPath)); maxWidth: 800,
$logoBase64 = 'data:image/'.pathinfo($logoPath, PATHINFO_EXTENSION).';base64,'.$logoData; maxHeight: 400,
} );
$image = $record->getFirstMedia('reports');
$imageBase64 = null;
if ($image) {
$imagePath = $image->getPath();
if (file_exists($imagePath)) {
$imageData = base64_encode(file_get_contents($imagePath));
$imageBase64 = 'data:image/'.pathinfo($imagePath, PATHINFO_EXTENSION).';base64,'.$imageData;
}
}
return response()->streamDownload(function () use ($record, $settings, $logoBase64, $imageBase64) { return response()->streamDownload(function () use ($record, $settings, $logoBase64, $imageBase64) {
set_time_limit(60);
echo Pdf::loadHtml( echo Pdf::loadHtml(
Blade::render('filament.manage.reports.print', [ Blade::render('filament.manage.reports.print', [
'record' => $record, 'record' => $record,

View File

@ -45,7 +45,6 @@ public static function configure(Schema $schema): Schema
->native(false) ->native(false)
->displayFormat('l, d F Y') ->displayFormat('l, d F Y')
->required() ->required()
->minDate(fn ($context) => $context === 'create' ? today() : null)
->reactive(), ->reactive(),
DatePicker::make('final_submission_date') DatePicker::make('final_submission_date')

View File

@ -46,8 +46,7 @@ public static function configure(Schema $schema): Schema
->label('Penulis') ->label('Penulis')
->placeholder('John Doe') ->placeholder('John Doe')
->autocomplete(false) ->autocomplete(false)
->required() ->required(),
->maxLength(50),
TextInput::make('link') TextInput::make('link')
->label('Tautan (URL)') ->label('Tautan (URL)')

View File

@ -58,6 +58,7 @@ public static function configure(Table $table): Table
TextColumn::make('writter') TextColumn::make('writter')
->label('Penulis') ->label('Penulis')
->limit(30)
->searchable() ->searchable()
->sortable(), ->sortable(),

View File

@ -0,0 +1,127 @@
<?php
namespace App\Filament\Support;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MediaDataUri
{
public static function fromMedia(?Media $media): ?string
{
if (! $media) {
return null;
}
$disk = Storage::disk($media->disk);
$path = $media->getPathRelativeToRoot();
if (! $disk->exists($path)) {
return null;
}
$mime = $media->mime_type ?: 'application/octet-stream';
return 'data:'.$mime.';base64,'.base64_encode($disk->get($path));
}
public static function fromPath(?string $absolutePath, int $maxWidth = 80, int $maxHeight = 80): ?string
{
if (! $absolutePath || ! file_exists($absolutePath)) {
return null;
}
$contents = file_get_contents($absolutePath);
if ($contents === false) {
return null;
}
return self::encodeResizedImage($contents, $maxWidth, $maxHeight)
?? self::encodeRawFile($absolutePath, $contents);
}
public static function thumbnailForPdf(?Media $media, int $maxWidth = 150, int $maxHeight = 80): ?string
{
if (! $media) {
return null;
}
$disk = Storage::disk($media->disk);
$path = $media->getPathRelativeToRoot();
if (! $disk->exists($path)) {
return null;
}
$mime = $media->mime_type ?? '';
if (! str_starts_with($mime, 'image/')) {
return null;
}
$contents = $disk->get($path);
return self::encodeResizedImage($contents, $maxWidth, $maxHeight)
?? self::fromMedia($media);
}
private static function encodeResizedImage(string $contents, int $maxWidth, int $maxHeight): ?string
{
if (! extension_loaded('gd')) {
return null;
}
$image = @imagecreatefromstring($contents);
if ($image === false) {
return null;
}
$width = imagesx($image);
$height = imagesy($image);
$ratio = min($maxWidth / $width, $maxHeight / $height, 1);
$newWidth = max(1, (int) round($width * $ratio));
$newHeight = max(1, (int) round($height * $ratio));
$thumb = imagecreatetruecolor($newWidth, $newHeight);
if ($thumb === false) {
imagedestroy($image);
return null;
}
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
imagedestroy($image);
ob_start();
imagejpeg($thumb, quality: 75);
imagedestroy($thumb);
$jpeg = ob_get_clean();
if ($jpeg === false || $jpeg === '') {
return null;
}
return 'data:image/jpeg;base64,'.base64_encode($jpeg);
}
private static function encodeRawFile(string $absolutePath, string $contents): ?string
{
$extension = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION));
$mime = match ($extension) {
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
default => null,
};
if ($mime === null) {
return null;
}
return 'data:'.$mime.';base64,'.base64_encode($contents);
}
}

View File

@ -0,0 +1,28 @@
<?php
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::table('media_monitorings', function (Blueprint $table) {
$table->text('writter')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('media_monitorings', function (Blueprint $table) {
$table->string('writter', 50)->change();
});
}
};

View File

@ -3,4 +3,4 @@
use Illuminate\Support\Facades\Schedule; use Illuminate\Support\Facades\Schedule;
Schedule::command('cooperation:update-status')->daily(); Schedule::command('cooperation:update-status')->daily();
Schedule::command('monitoring:crawl')->dailyAt('12:00'); Schedule::command('monitoring:crawl')->daily();

View File

@ -20,12 +20,13 @@
expect(CooperationStatus::options())->toEqual([ expect(CooperationStatus::options())->toEqual([
1 => 'Menunggu', 1 => 'Menunggu',
2 => 'Media Order', 2 => 'Media Order',
4 => 'Pembayaran', 3 => 'Pembayaran',
5 => 'Selesai', 4 => 'Selesai',
]); ]);
}); });
test('cooperation status enum values and comment', function () { test('cooperation status enum values and comment', function () {
expect(CooperationStatus::values())->toEqual([1, 2, 4, 5]); expect(CooperationStatus::values())->toEqual([1, 2, 3, 4]);
expect(CooperationStatus::comment())->toBe('1: Menunggu, 2: Media Order, 4: Pembayaran, 5: Selesai'); expect(CooperationStatus::comment())->toBe('1: Menunggu, 2: Media Order, 3: Pembayaran, 4: Selesai');
}); });