Compare commits

..

No commits in common. "925fbe0bd76fd222a8feaea9d5b29fc74cd0227d" and "382dc4aa03f89875d23da0e0ca4a6fc77e9c5f20" have entirely different histories.

13 changed files with 65 additions and 322 deletions

View File

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

View File

@ -381,24 +381,6 @@ private function processReport(object $legacyReport, int $cooperationId, int $ta
[$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([
'media_task_assignment_id' => $mediaTaskAssignment->id,
'title' => $cleanTitle,
@ -412,13 +394,13 @@ private function processReport(object $legacyReport, int $cooperationId, int $ta
$newReportId = (int) DB::getPdo()->lastInsertId();
if (! isset($this->migratedReportIds[$newReportId])) {
$report = Report::find($newReportId);
if ($report) {
$this->migrateProof($report, $legacyReport);
$this->migratedReportIds[$newReportId] = true;
}
}
// // Migrate proof image if not already done
// if (! isset($this->migratedReportIds[$newReportId])) {
// $report = Report::find($newReportId);
// if ($report) {
// $this->migrateProof($report, $legacyReport);
// }
// }
}
/**

View File

@ -134,26 +134,12 @@ private function processReport(object $legacy): void
return;
}
[$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;
}
// Insert the report
DB::table('reports')->insert([
'media_task_assignment_id' => $mediaTaskAssignment->id,
'title' => $cleanTitle,
'title' => $legacy->title ?? '',
'publication_date' => $legacy->broadcast,
'link' => $cleanLink,
'link' => $legacy->link ?? '',
'description' => '',
'status' => $this->mapApprovalStatus((string) $legacy->status),
'created_at' => $legacy->created_at ?? now(),
@ -162,46 +148,32 @@ private function processReport(object $legacy): void
$newReportId = (int) DB::getPdo()->lastInsertId();
$proofMigrated = $this->migrateProofIfNeeded($newReportId, $legacy);
// Migrate proof image if not already done
if (! isset($this->migratedIds[$newReportId])) {
$report = Report::find($newReportId);
if ($report) {
$this->migrateProof($report, $legacy);
}
}
$this->output->writeln($proofMigrated ? '<info>DONE + IMG</info>' : '<info>DONE</info>');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Register proof media for a report when not yet migrated.
* Migrate the proof image file from S3 to the Report media library.
*/
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
private function migrateProof(Report $report, object $legacy): void
{
$legacyPath = trim($legacy->proof ?? '');
if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
return false;
return;
}
$sourcePath = 'old-data/reports/'.$legacyPath;
$this->importProofFile($report, $sourcePath, $legacy);
$this->migratedIds[$report->id] = true;
return true;
}
/**
@ -250,45 +222,6 @@ 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.
* Legacy: 0 = menunggu, 1 = menerima, 2 = menolak

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,127 +0,0 @@
<?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

@ -1,28 +0,0 @@
<?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;
Schedule::command('cooperation:update-status')->daily();
Schedule::command('monitoring:crawl')->daily();
Schedule::command('monitoring:crawl')->dailyAt('12:00');

View File

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