diff --git a/app/Console/Commands/MigrateCooperationCommand.php b/app/Console/Commands/MigrateCooperationCommand.php
deleted file mode 100644
index 3abb791..0000000
--- a/app/Console/Commands/MigrateCooperationCommand.php
+++ /dev/null
@@ -1,320 +0,0 @@
- new cooperation_id
- * Dipakai untuk lookup saat migrate proposals
- */
- private array $legacyCoopMap = [];
-
- public function handle(): void
- {
- $this->migrateMediaOrders();
- $this->migrateMediaCooperation();
- $this->migrateProposals();
- $this->migrateReports();
- }
-
- private function migrateMediaOrders(): void
- {
- $this->info('Migrating media_orders → cooperations...');
-
- $groups = DB::connection('mysql_second')
- ->table('media_orders')
- ->whereNull('deleted_at')
- ->orderBy('id')
- ->get()
- ->groupBy(fn ($item) => strtolower(trim($item->name)));
-
- foreach ($groups as $group) {
- $first = $group->first();
- $cooperationId = $this->insertCooperation(
- name: $first->name,
- description: $first->description,
- initialDate: $group->min('begin'),
- finalDate: $group->max('end'),
- status: CooperationStatus::COMPLETED->value,
- completedAt: $group->max('end'),
- );
-
- $this->insertCooperationMedia(
- group: $group,
- cooperationId: $cooperationId,
- approvalStatus: ApprovalStatus::ACCEPTED->value,
- );
-
- $startDate = Carbon::parse($group->max('end'))->addDay();
- $endDate = $startDate->copy()->addDays(7);
-
- DB::table('task_assignments')->insert([
- 'cooperation_id' => $cooperationId,
- 'start_date' => $startDate->toDateString(),
- 'end_date' => $endDate->toDateString(),
- 'task_description' => $first->description ?? '',
- 'report_amount' => $group->sum('amount_report') ?: 1,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
-
- $taskAssignmentId = (int) DB::getPdo()->lastInsertId();
-
- $this->insertMediaTaskAssignment(
- cooperationId: $cooperationId,
- taskAssignmentId: $taskAssignmentId,
- approvalStatus: ApprovalStatus::ACCEPTED->value,
- );
-
- $this->info("Merged {$group->count()} rows → {$first->name}");
- }
-
- $this->info('media_orders migration done.');
- }
-
- private function migrateMediaCooperation(): void
- {
- $this->info('Migrating media_cooperation → cooperations...');
-
- $groups = DB::connection('mysql_second')
- ->table('media_cooperation')
- ->whereNull('deleted_at')
- ->orderBy('id')
- ->get()
- ->groupBy(fn ($item) => strtolower(trim($item->name)));
-
- foreach ($groups as $group) {
- $first = $group->first();
- $cooperationId = $this->insertCooperation(
- name: $first->name,
- description: $first->description,
- initialDate: $group->min('begin'),
- finalDate: $group->max('end'),
- status: CooperationStatus::PENDING->value,
- completedAt: null,
- );
-
- $this->insertCooperationMedia(
- group: $group,
- cooperationId: $cooperationId,
- approvalStatus: ApprovalStatus::PENDING->value,
- );
-
- $startDate = Carbon::parse($group->max('end'))->addDay();
- $endDate = $startDate->copy()->addDays(7);
-
- DB::table('task_assignments')->insert([
- 'cooperation_id' => $cooperationId,
- 'start_date' => $startDate->toDateString(),
- 'end_date' => $endDate->toDateString(),
- 'task_description' => $first->description ?? '',
- 'report_amount' => 1,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
-
- $taskAssignmentId = (int) DB::getPdo()->lastInsertId();
-
- $this->insertMediaTaskAssignment(
- cooperationId: $cooperationId,
- taskAssignmentId: $taskAssignmentId,
- approvalStatus: ApprovalStatus::PENDING->value,
- );
-
- // Simpan mapping legacy id → new cooperation_id untuk dipakai saat migrate proposals
- foreach ($group as $item) {
- $this->legacyCoopMap[$item->id] = $cooperationId;
- }
-
- $this->info("Merged {$group->count()} rows → {$first->name}");
- }
-
- $this->info('media_cooperation migration done.');
- }
-
- private function migrateProposals(): void
- {
- $this->info('Migrating propose_media_cooperation → cooperation_proposals...');
-
- $proposals = DB::connection('mysql_second')
- ->table('propose_media_cooperation')
- ->whereNull('deleted_at')
- ->get();
-
- foreach ($proposals as $proposal) {
- $cooperationId = $this->legacyCoopMap[$proposal->media_cooperation_id] ?? null;
-
- if (! $cooperationId) {
- $this->warn("cooperation not found for proposal {$proposal->id} (media_cooperation_id: {$proposal->media_cooperation_id})");
-
- continue;
- }
-
- $partnerMediaId = DB::table('partner_media')
- ->join('companies', 'partner_media.company_id', '=', 'companies.id')
- ->where('companies.user_id', $proposal->enhancer)
- ->value('partner_media.id');
-
- if (! $partnerMediaId) {
- $this->warn("partner_media not found for proposal {$proposal->id} (enhancer/user_id: {$proposal->enhancer})");
-
- continue;
- }
-
- DB::table('cooperation_proposals')->insert([
- 'cooperation_id' => $cooperationId,
- 'partner_media_id' => $partnerMediaId,
- 'description' => null,
- 'e_catalog' => null,
- 'status' => $this->mapApprovalStatus($proposal->status),
- 'submitted_at' => $proposal->created_at ?? now(),
- 'responded_at' => $proposal->updated_at,
- 'created_at' => $proposal->created_at ?? now(),
- 'updated_at' => $proposal->updated_at ?? now(),
- ]);
- }
-
- $this->info('proposals migration done.');
- }
-
- // =========================================================
- // Helper Methods
- // =========================================================
-
- private function insertCooperation(
- string $name,
- ?string $description,
- string $initialDate,
- string $finalDate,
- string $status,
- ?string $completedAt,
- ): int {
- DB::table('cooperations')->insert([
- 'title' => $name,
- 'initial_submission_date' => $initialDate,
- 'final_submission_date' => $finalDate,
- 'description' => $description ?? '',
- 'payment_amount' => null,
- 'payment_date' => null,
- 'completed_at' => $completedAt,
- 'status' => $status,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
-
- return (int) DB::getPdo()->lastInsertId();
- }
-
- private function insertCooperationMedia(
- Collection $group,
- int $cooperationId,
- string $approvalStatus,
- ): void {
- $validMediaIds = DB::table('partner_media')->pluck('id')->toArray();
-
- $mediaIds = collect();
-
- foreach ($group as $item) {
- if (! $item->media) {
- continue;
- }
- $mediaIds = $mediaIds->merge(explode(',', $item->media));
- }
-
- $mediaIds
- ->map(fn ($id) => (int) $id)
- ->filter(fn ($id) => in_array($id, $validMediaIds))
- ->unique()
- ->each(function ($mediaId) use ($cooperationId, $approvalStatus) {
- DB::table('cooperation_media')->insert([
- 'cooperation_id' => $cooperationId,
- 'partner_media_id' => $mediaId,
- 'status' => $approvalStatus,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- });
- }
-
- private function mapApprovalStatus(string $legacy): string
- {
- return match ($legacy) {
- 'accepted' => ApprovalStatus::ACCEPTED->value,
- 'rejected' => ApprovalStatus::REJECTED->value,
- default => ApprovalStatus::PENDING->value,
- };
- }
-
- private function insertMediaTaskAssignment(
- int $cooperationId,
- int $taskAssignmentId,
- string $approvalStatus,
- ): void {
- $mediaIds = DB::table('cooperation_media')
- ->where('cooperation_id', $cooperationId)
- ->pluck('partner_media_id');
-
- foreach ($mediaIds as $partnerMediaId) {
- DB::table('media_task_assignment')->insert([
- 'task_assignment_id' => $taskAssignmentId,
- 'partner_media_id' => $partnerMediaId,
- 'status' => $approvalStatus,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- }
-
- private function migrateReports(): void
- {
- $this->info('Migrating reports → reports...');
-
- $legacyReports = DB::connection('mysql_second')
- ->table('reports')
- ->whereNull('deleted_at')
- ->get();
-
- foreach ($legacyReports as $legacy) {
- // Cari media_task_assignment berdasarkan partner_media_id
- // dan task_assignment yang berelasi ke cooperation dengan tanggal yang sesuai
- $mediaTaskAssignment = DB::table('media_task_assignment')
- ->join('task_assignments', 'media_task_assignment.task_assignment_id', '=', 'task_assignments.id')
- ->where('media_task_assignment.partner_media_id', $legacy->media)
- ->whereDate('task_assignments.start_date', '<=', $legacy->broadcast)
- ->whereDate('task_assignments.end_date', '>=', $legacy->broadcast)
- ->select('media_task_assignment.id')
- ->first();
-
- if (! $mediaTaskAssignment) {
- $this->warn("media_task_assignment not found for report {$legacy->id}");
-
- continue;
- }
-
- DB::table('reports')->insert([
- 'media_task_assignment_id' => $mediaTaskAssignment->id,
- 'title' => $legacy->title,
- 'publication_date' => $legacy->broadcast,
- 'link' => $legacy->link,
- 'description' => $legacy->proof ?? '',
- 'status' => $this->mapApprovalStatus($legacy->status),
- 'created_at' => $legacy->created_at ?? now(),
- 'updated_at' => $legacy->updated_at ?? now(),
- ]);
- }
-
- $this->info('reports migration done.');
- }
-}
diff --git a/app/Console/Commands/MigrateMediaCooperationCommand.php b/app/Console/Commands/MigrateMediaCooperationCommand.php
new file mode 100644
index 0000000..42393b6
--- /dev/null
+++ b/app/Console/Commands/MigrateMediaCooperationCommand.php
@@ -0,0 +1,310 @@
+info('Starting migration: media_cooperation → cooperations...');
+
+ $legacyConn = DB::connection('mysql_second');
+ $query = $legacyConn->table('media_cooperation')->whereNull('deleted_at');
+
+ try {
+ $totalCount = $query->count();
+ } catch (\Exception $e) {
+ $this->error('Table media_cooperation not found or error accessing legacy database.');
+ $this->error($e->getMessage());
+
+ return;
+ }
+
+ if ($totalCount === 0) {
+ $this->warn('No media_cooperation found in legacy database.');
+
+ return;
+ }
+
+ $this->info("Found {$totalCount} legacy media_cooperation records.");
+
+ // Pre-cache migrated cooperation titles
+ $this->migratedTitles = DB::table('cooperations')
+ ->pluck('title')
+ ->map(fn ($t) => strtolower(trim($t)))
+ ->flip()
+ ->all();
+
+ // Pre-cache proposal media
+ $this->migratedProposalIds = DB::table('media')
+ ->where('model_type', CooperationProposal::class)
+ ->where('collection_name', 'proposals')
+ ->pluck('model_id')
+ ->flip()
+ ->all();
+
+ // Pre-load User to Partner Media mapping
+ $this->loadUserToMediaMap();
+
+ // Load all records and group by name
+ $all = $query->orderBy('id')->get();
+ $groups = $all->groupBy(fn ($item) => strtolower(trim($item->name)));
+
+ $this->info("Grouped into {$groups->count()} unique cooperation group(s).");
+
+ foreach ($groups as $groupKey => $group) {
+ $this->processGroup($groupKey, $group);
+ }
+
+ $this->newLine();
+ $this->info('media_cooperation migration completed successfully!');
+ }
+
+ /**
+ * Load mapping from User ID to Partner Media ID.
+ * Path: User -> Company -> PartnerMedia
+ */
+ private function loadUserToMediaMap(): void
+ {
+ $this->userToMediaMap = DB::table('partner_media')
+ ->join('companies', 'partner_media.company_id', '=', 'companies.id')
+ ->whereNotNull('companies.user_id')
+ ->pluck('partner_media.id', 'companies.user_id')
+ ->all();
+ }
+
+ /**
+ * Process a single group of media_cooperation records.
+ */
+ private function processGroup(string $groupKey, Collection $group): void
+ {
+ $first = $group->first();
+ $label = Str::limit($first->name, 40);
+ $this->output->write("[group] Processing: {$label}... ");
+
+ try {
+ // Check if cooperation already exists by title
+ $existingCooperation = DB::table('cooperations')
+ ->whereRaw('LOWER(TRIM(title)) = ?', [$groupKey])
+ ->first();
+
+ if ($existingCooperation) {
+ $cooperationId = $existingCooperation->id;
+ $this->output->write('JOINING EXISTING... ');
+ } else {
+ $cooperationId = $this->insertCooperation(
+ name: $first->name,
+ description: $first->description,
+ initialDate: $group->min('begin'),
+ finalDate: $group->max('end'),
+ status: CooperationStatus::COMPLETED->value,
+ );
+ $this->output->write('CREATED NEW... ');
+ }
+
+ // Optional: insert cooperation_media if the legacy table has a 'media' column with IDs
+ if (isset($first->media)) {
+ $this->insertCooperationMedia($group, $cooperationId);
+ }
+
+ // Migrate proposals belonging to this group's media_cooperation
+ $this->migrateProposals(
+ group: $group,
+ cooperationId: $cooperationId
+ );
+
+ $this->output->writeln('DONE');
+ } catch (\Exception $e) {
+ $this->output->writeln('FAILED: '.$e->getMessage().'');
+ }
+ }
+
+ private function insertCooperation(
+ string $name,
+ ?string $description,
+ ?string $initialDate,
+ ?string $finalDate,
+ string $status
+ ): int {
+ DB::table('cooperations')->insert([
+ 'title' => $name,
+ 'initial_submission_date' => $initialDate,
+ 'final_submission_date' => $finalDate,
+ 'description' => $description ?? '',
+ 'status' => $status,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ return (int) DB::getPdo()->lastInsertId();
+ }
+
+ private function insertCooperationMedia(Collection $group, int $cooperationId): void
+ {
+ $validMediaIds = DB::table('partner_media')->pluck('id')->toArray();
+ $mediaStatusMap = [];
+
+ foreach ($group as $item) {
+ if (empty($item->media)) {
+ continue;
+ }
+
+ $ids = array_map('trim', explode(',', $item->media));
+ $statuses = array_map('trim', explode(',', $item->status ?? ''));
+
+ foreach ($ids as $index => $rawId) {
+ $mediaId = (int) $rawId;
+ if (! in_array($mediaId, $validMediaIds)) {
+ continue;
+ }
+
+ $legacyStatus = $statuses[$index] ?? '1';
+ $mediaStatusMap[$mediaId] = $this->mapApprovalStatus($legacyStatus);
+ }
+ }
+
+ foreach ($mediaStatusMap as $mediaId => $approvalStatus) {
+ DB::table('cooperation_media')->insert([
+ 'cooperation_id' => $cooperationId,
+ 'partner_media_id' => $mediaId,
+ 'status' => $approvalStatus,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ }
+
+ private function migrateProposals(Collection $group, int $cooperationId): void
+ {
+ $legacyConn = DB::connection('mysql_second');
+ $legacyIds = $group->pluck('id')->toArray();
+
+ $legacyProposals = $legacyConn->table('propose_media_cooperation')
+ ->whereIn('media_cooperation_id', $legacyIds)
+ ->whereNull('deleted_at')
+ ->get();
+
+ foreach ($legacyProposals as $legacyProposal) {
+ $this->processProposal($legacyProposal, $cooperationId);
+ }
+ }
+
+ private function processProposal(object $legacy, int $cooperationId): void
+ {
+ $userId = $legacy->enhancer;
+ $partnerMediaId = $this->userToMediaMap[$userId] ?? null;
+
+ if (! $partnerMediaId) {
+ // Fallback or skip if media not found
+ return;
+ }
+
+ // Check if this proposal already exists to avoid duplication
+ $exists = DB::table('cooperation_proposals')
+ ->where('cooperation_id', $cooperationId)
+ ->where('partner_media_id', $partnerMediaId)
+ ->exists();
+
+ if ($exists) {
+ return;
+ }
+
+ DB::table('cooperation_proposals')->insert([
+ 'cooperation_id' => $cooperationId,
+ 'partner_media_id' => $partnerMediaId,
+ 'status' => $this->mapApprovalStatus((string) $legacy->status),
+ 'submitted_at' => $legacy->created_at ?? now(),
+ 'created_at' => $legacy->created_at ?? now(),
+ 'updated_at' => $legacy->updated_at ?? now(),
+ ]);
+
+ $proposalId = (int) DB::getPdo()->lastInsertId();
+
+ // Migrate offer_file if exists and not already migrated
+ if (! empty($legacy->offer_file) && ! isset($this->migratedProposalIds[$proposalId])) {
+ $this->importOfferFile($proposalId, $legacy);
+ }
+ }
+
+ private function importOfferFile(int $proposalId, object $legacy): void
+ {
+ $legacyPath = trim($legacy->offer_file);
+ if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
+ return;
+ }
+
+ // Adjust path based on where these files are stored in S3
+ $sourcePath = 'old-data/proposals/'.$legacyPath;
+
+ try {
+ if (Storage::disk('s3')->exists($sourcePath)) {
+ $proposal = CooperationProposal::find($proposalId);
+ if ($proposal) {
+ $proposal->addMediaFromDisk($sourcePath, 's3')
+ ->withCustomProperties([
+ 'feature' => 'proposals',
+ 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
+ ])
+ ->preservingOriginal()
+ ->toMediaCollection('proposals', 's3');
+ }
+ }
+ } catch (\Exception $e) {
+ // Log silently
+ }
+ }
+
+ private function mapApprovalStatus(string $legacyStatus): int
+ {
+ return match ($legacyStatus) {
+ '0' => ApprovalStatus::PENDING->value,
+ '1' => ApprovalStatus::ACCEPTED->value,
+ '2' => ApprovalStatus::REJECTED->value,
+ default => ApprovalStatus::ACCEPTED->value,
+ };
+ }
+}
diff --git a/app/Console/Commands/MigrateMediaOrdersCommand.php b/app/Console/Commands/MigrateMediaOrdersCommand.php
new file mode 100644
index 0000000..ff02076
--- /dev/null
+++ b/app/Console/Commands/MigrateMediaOrdersCommand.php
@@ -0,0 +1,519 @@
+info('Starting migration: media_orders → cooperations...');
+
+ $legacyConn = DB::connection('mysql_second');
+ $query = $legacyConn->table('media_orders')->whereNull('deleted_at');
+
+ $totalCount = $query->count();
+ if ($totalCount === 0) {
+ $this->warn('No media_orders found in legacy database.');
+
+ return;
+ }
+
+ $this->info("Found {$totalCount} legacy media_order records.");
+
+ // Pre-cache migrated cooperation titles to skip already processed groups
+ $this->migratedTitles = DB::table('cooperations')
+ ->pluck('title')
+ ->map(fn ($t) => strtolower(trim($t)))
+ ->flip()
+ ->all();
+
+ // Pre-cache cooperation IDs that already have an offer file
+ $this->migratedOfferIds = DB::table('media')
+ ->where('model_type', Cooperation::class)
+ ->where('collection_name', 'cooperations')
+ ->pluck('model_id')
+ ->flip()
+ ->all();
+
+ // Pre-cache report IDs that already have proof media (skip re-importing files)
+ $this->migratedReportIds = DB::table('media')
+ ->where('model_type', Report::class)
+ ->where('collection_name', 'reports')
+ ->pluck('model_id')
+ ->flip()
+ ->all();
+
+ // Load all records first (needed for groupBy), then process in chunks via collection
+ $all = $query->orderBy('id')->get();
+ $groups = $all->groupBy(fn ($item) => strtolower(trim($item->name)));
+
+ $this->info("Grouped into {$groups->count()} unique cooperation group(s).");
+
+ foreach ($groups->chunk(50) as $chunk) {
+ foreach ($chunk as $groupKey => $group) {
+ $this->processGroup($groupKey, $group);
+ }
+ }
+
+ $this->newLine();
+ $this->info('media_orders migration completed successfully!');
+ }
+
+ /**
+ * Process a single group of media_order records (one cooperation).
+ */
+ private function processGroup(string $groupKey, Collection $group): void
+ {
+ $first = $group->first();
+ $label = Str::limit($first->name, 40);
+ $this->output->write("[group] Processing: {$label}... ");
+
+ try {
+ // Skip if already migrated
+ if (isset($this->migratedTitles[$groupKey])) {
+ $this->output->writeln('SKIP (already migrated)');
+
+ return;
+ }
+
+ $startDate = Carbon::parse($group->max('end'))->addDay();
+ $endDate = $startDate->copy()->addDays(7);
+ $paymentDate = $endDate->copy()->addDays(1);
+ $completedAt = $paymentDate->copy()->addDays(7);
+
+ $cooperationId = $this->insertCooperation(
+ name: $first->name,
+ description: $first->description,
+ initialDate: $group->min('begin'),
+ finalDate: $group->max('end'),
+ status: CooperationStatus::COMPLETED->value,
+ paymentDate: $paymentDate->toDateString(),
+ completedAt: $completedAt->toDateString(),
+ );
+
+ $this->insertCooperationMedia(
+ group: $group,
+ cooperationId: $cooperationId,
+ );
+
+ DB::table('task_assignments')->insert([
+ 'cooperation_id' => $cooperationId,
+ 'start_date' => $startDate->toDateString(),
+ 'end_date' => $endDate->toDateString(),
+ 'task_description' => $first->description ?? '',
+ 'report_amount' => $group->sum('amount_report'),
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $taskAssignmentId = (int) DB::getPdo()->lastInsertId();
+
+ $this->insertMediaTaskAssignment(
+ cooperationId: $cooperationId,
+ taskAssignmentId: $taskAssignmentId,
+ approvalStatus: ApprovalStatus::ACCEPTED->value,
+ );
+
+ // // Migrate offer_file_template documents for each item in the group
+ // if (! isset($this->migratedOfferIds[$cooperationId])) {
+ // $cooperation = Cooperation::find($cooperationId);
+ // if ($cooperation) {
+ // $this->migrateOfferFiles($cooperation, $group);
+ // }
+ // }
+
+ // Migrate reports belonging to this group's media_orders
+ $this->migrateReports(
+ group: $group,
+ cooperationId: $cooperationId,
+ taskAssignmentId: $taskAssignmentId,
+ );
+
+ // Cache so re-runs skip this title
+ $this->migratedTitles[$groupKey] = true;
+
+ $this->output->writeln('DONE');
+ } catch (\Exception $e) {
+ $this->output->writeln('FAILED: '.$e->getMessage().'');
+ }
+ }
+
+ // =========================================================
+ // Helper Methods
+ // =========================================================
+
+ private function insertCooperation(
+ string $name,
+ ?string $description,
+ string $initialDate,
+ string $finalDate,
+ string $status,
+ ?string $paymentDate,
+ ?string $completedAt,
+ ): int {
+ DB::table('cooperations')->insert([
+ 'title' => $name,
+ 'initial_submission_date' => $initialDate,
+ 'final_submission_date' => $finalDate,
+ 'description' => $description ?? '',
+ 'payment_amount' => null,
+ 'payment_date' => $paymentDate,
+ 'completed_at' => $completedAt,
+ 'status' => $status,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ return (int) DB::getPdo()->lastInsertId();
+ }
+
+ private function insertCooperationMedia(
+ Collection $group,
+ int $cooperationId,
+ ): void {
+ $validMediaIds = DB::table('partner_media')->pluck('id')->toArray();
+
+ // Build a map of mediaId → ApprovalStatus from all items in the group.
+ // Columns 'media' and 'status' are parallel comma-separated arrays:
+ // e.g. media="1,2,3" status="1,0,2" means media 1=accepted, 2=pending, 3=rejected
+ $mediaStatusMap = [];
+
+ foreach ($group as $item) {
+ if (! $item->media) {
+ continue;
+ }
+
+ $ids = array_map('trim', explode(',', $item->media));
+ $statuses = array_map('trim', explode(',', $item->status ?? ''));
+
+ foreach ($ids as $index => $rawId) {
+ $mediaId = (int) $rawId;
+ $legacyStatus = $statuses[$index] ?? '0';
+
+ if (! in_array($mediaId, $validMediaIds)) {
+ $this->output->writeln(
+ " ↳ media #{$mediaId} SKIP (tidak ditemukan di tabel partner_media, report-nya akan ikut hilang)>"
+ );
+
+ continue;
+ }
+
+ // Last status seen wins (latest record in group takes precedence)
+ $mediaStatusMap[$mediaId] = $this->mapApprovalStatus($legacyStatus);
+ }
+ }
+
+ foreach ($mediaStatusMap as $mediaId => $approvalStatus) {
+ DB::table('cooperation_media')->insert([
+ 'cooperation_id' => $cooperationId,
+ 'partner_media_id' => $mediaId,
+ 'status' => $approvalStatus,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ }
+
+ /**
+ * Migrate offer_file_template files for each item in the group to the Cooperation media library.
+ */
+ private function migrateOfferFiles(Cooperation $cooperation, Collection $group): void
+ {
+ foreach ($group as $item) {
+ $legacyPath = trim($item->offer_file_template ?? '');
+ if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
+ continue;
+ }
+
+ $sourcePath = 'old-data/mediaorders/'.$legacyPath;
+ $this->importOfferFile($cooperation, $sourcePath, $item);
+ }
+ }
+
+ /**
+ * Import a single offer file to the Cooperation media library.
+ */
+ private function importOfferFile(Cooperation $cooperation, string $sourcePath, object $item): void
+ {
+ $attempts = 0;
+ $maxAttempts = 3;
+ $exists = false;
+
+ while ($attempts < $maxAttempts) {
+ try {
+ $exists = Storage::disk('s3')->exists($sourcePath);
+ break;
+ } catch (\Exception $e) {
+ $attempts++;
+ usleep(200000);
+ }
+ }
+
+ if (! $exists) {
+ return;
+ }
+
+ try {
+ $cooperation->addMediaFromDisk($sourcePath, 's3')
+ ->withCustomProperties([
+ 'feature' => 'cooperations',
+ 'date' => Carbon::parse($item->created_at ?? now())->toDateString(),
+ 'doc_type' => 'proposal-template',
+ ])
+ ->preservingOriginal()
+ ->toMediaCollection('cooperations', 's3');
+ } catch (\Exception $e) {
+ // Log quietly or handle error
+ }
+ }
+
+ private function insertMediaTaskAssignment(
+ int $cooperationId,
+ int $taskAssignmentId,
+ string $approvalStatus,
+ ): void {
+ $mediaIds = DB::table('cooperation_media')
+ ->where('cooperation_id', $cooperationId)
+ ->pluck('partner_media_id');
+
+ foreach ($mediaIds as $partnerMediaId) {
+ DB::table('media_task_assignment')->insert([
+ 'task_assignment_id' => $taskAssignmentId,
+ 'partner_media_id' => $partnerMediaId,
+ 'status' => $approvalStatus,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ }
+
+ /**
+ * Migrate all legacy reports belonging to the given group of media_orders.
+ * Each item in the group can have multiple reports via reports.media_order = item.id.
+ */
+ private function migrateReports(Collection $group, int $cooperationId, int $taskAssignmentId): void
+ {
+ $legacyConn = DB::connection('mysql_second');
+
+ // Collect all legacy media_order IDs in this group
+ $legacyOrderIds = $group->pluck('id')->toArray();
+
+ $legacyReports = $legacyConn
+ ->table('reports')
+ ->whereNull('deleted_at')
+ ->whereIn('media_order', $legacyOrderIds)
+ ->orderBy('id')
+ ->get();
+
+ if ($legacyReports->isEmpty()) {
+ return;
+ }
+
+ foreach ($legacyReports as $legacyReport) {
+ $this->processReport(
+ legacyReport: $legacyReport,
+ cooperationId: $cooperationId,
+ taskAssignmentId: $taskAssignmentId,
+ );
+ }
+ }
+
+ /**
+ * Process a single legacy report record.
+ */
+ private function processReport(object $legacyReport, int $cooperationId, int $taskAssignmentId): void
+ {
+ $partnerMediaId = (int) $legacyReport->media;
+
+ // Resolve media_task_assignment via partner_media_id = legacyReport.media
+ $mediaTaskAssignment = DB::table('media_task_assignment')
+ ->where('task_assignment_id', $taskAssignmentId)
+ ->where('partner_media_id', $partnerMediaId)
+ ->select('id')
+ ->first();
+
+ if (! $mediaTaskAssignment) {
+ $this->output->writeln(
+ " ↳ report #{$legacyReport->id} SKIP (media #{$partnerMediaId} tidak terdaftar di kerja sama ini — tidak ada di media_orders.media)>"
+ );
+
+ return;
+ }
+
+ [$cleanTitle, $cleanLink] = $this->extractLinkData($legacyReport);
+
+ DB::table('reports')->insert([
+ 'media_task_assignment_id' => $mediaTaskAssignment->id,
+ 'title' => $cleanTitle,
+ 'publication_date' => $legacyReport->broadcast,
+ 'link' => $cleanLink,
+ 'description' => '',
+ 'status' => $this->mapApprovalStatus((string) $legacyReport->status),
+ 'created_at' => $legacyReport->created_at ?? now(),
+ 'updated_at' => $legacyReport->updated_at ?? now(),
+ ]);
+
+ $newReportId = (int) DB::getPdo()->lastInsertId();
+
+ // // Migrate proof image if not already done
+ // if (! isset($this->migratedReportIds[$newReportId])) {
+ // $report = Report::find($newReportId);
+ // if ($report) {
+ // $this->migrateProof($report, $legacyReport);
+ // }
+ // }
+ }
+
+ /**
+ * Ekstrak title & link bersih dari legacy report.
+ *
+ * Di DB lama kolom sering terbalik:
+ * - reports.title → berisi URL artikel
+ * - reports.link → berisi teks panjang (judul + isi + URL)
+ *
+ * Logika:
+ * 1. Jika title terdeteksi sebagai URL → title = baris pertama link (judul artikel)
+ * link = URL dari title
+ * 2. Jika tidak → ekstrak URL dari teks link, title tetap apa adanya (max 255 char)
+ *
+ * @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);
+
+ // Cari URL pertama di dalam teks rawLink
+ preg_match('#https?://\S+#', $rawLink, $urlMatches);
+ $extractedUrl = rtrim($urlMatches[0] ?? '', '.,;)');
+
+ if ($titleIsUrl) {
+ // title berisi URL → pakai sebagai link
+ $cleanLink = $rawTitle;
+
+ // Ambil judul dari baris pertama rawLink yang bukan URL
+ $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 {
+ // Normal: title sudah benar; ekstrak URL bersih dari link
+ $cleanLink = $extractedUrl ?: mb_substr($rawLink, 0, 2048);
+ $cleanTitle = mb_substr($rawTitle, 0, 255);
+ }
+
+ return [$cleanTitle, $cleanLink];
+ }
+
+ /**
+ * Migrate the proof image file from S3 to the Report media library.
+ */
+ private function migrateProof(Report $report, object $legacy): void
+ {
+ $legacyPath = trim($legacy->proof ?? '');
+ if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
+ return;
+ }
+
+ $sourcePath = 'old-data/reports/'.$legacyPath;
+
+ $attempts = 0;
+ $maxAttempts = 3;
+ $exists = false;
+
+ while ($attempts < $maxAttempts) {
+ try {
+ $exists = Storage::disk('s3')->exists($sourcePath);
+ break;
+ } catch (\Exception $e) {
+ $attempts++;
+ usleep(200000);
+ }
+ }
+
+ if (! $exists) {
+ return;
+ }
+
+ try {
+ $report->addMediaFromDisk($sourcePath, 's3')
+ ->withCustomProperties([
+ 'feature' => 'reports',
+ 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
+ 'doc_type' => 'proof',
+ ])
+ ->preservingOriginal()
+ ->toMediaCollection('reports', 's3');
+ } catch (\Exception $e) {
+ // Log quietly or handle error
+ }
+ }
+
+ /**
+ * Map legacy status integer to ApprovalStatus enum value.
+ * Legacy: 0 = menunggu, 1 = menerima, 2 = menolak
+ */
+ private function mapApprovalStatus(string $legacyStatus): int
+ {
+ return match ($legacyStatus) {
+ '0' => ApprovalStatus::PENDING->value,
+ '1' => ApprovalStatus::ACCEPTED->value,
+ '2' => ApprovalStatus::REJECTED->value,
+ default => ApprovalStatus::ACCEPTED->value,
+ };
+ }
+}
diff --git a/app/Console/Commands/MigrateReportsCommand.php b/app/Console/Commands/MigrateReportsCommand.php
new file mode 100644
index 0000000..63e8be4
--- /dev/null
+++ b/app/Console/Commands/MigrateReportsCommand.php
@@ -0,0 +1,230 @@
+info('Starting migration: reports → reports...');
+
+ $legacyConn = DB::connection('mysql_second');
+ $query = $legacyConn->table('reports')->whereNull('deleted_at');
+
+ $totalCount = $query->count();
+ if ($totalCount === 0) {
+ $this->warn('No reports found in legacy database.');
+
+ return;
+ }
+
+ $this->info("Found {$totalCount} legacy report record(s).");
+
+ // Pre-cache report IDs that already have proof media (skip re-importing files)
+ $this->migratedIds = DB::table('media')
+ ->where('model_type', Report::class)
+ ->where('collection_name', 'reports')
+ ->pluck('model_id')
+ ->flip()
+ ->all();
+
+ // Build a lookup: legacy media_orders.id → new cooperations.id
+ // via: media_orders.name (lowercased) → cooperations.title (lowercased)
+ $legacyOrders = $legacyConn->table('media_orders')
+ ->whereNull('deleted_at')
+ ->select('id', 'name')
+ ->get()
+ ->keyBy('id');
+
+ $cooperationMap = DB::table('cooperations')
+ ->select('id', 'title')
+ ->get()
+ ->keyBy(fn ($c) => strtolower(trim($c->title)));
+
+ foreach ($legacyOrders as $order) {
+ $key = strtolower(trim($order->name));
+ if (isset($cooperationMap[$key])) {
+ $this->mediaOrderToCooperation[$order->id] = $cooperationMap[$key]->id;
+ }
+ }
+
+ $query->orderBy('id')->chunk(50, function ($chunk) {
+ foreach ($chunk as $legacy) {
+ $this->processReport($legacy);
+ }
+ });
+
+ $this->newLine();
+ $this->info('reports migration completed successfully!');
+ }
+
+ /**
+ * Process a single legacy report record.
+ */
+ private function processReport(object $legacy): void
+ {
+ $idText = "[#{$legacy->id}]";
+ $this->output->write("{$idText} Processing: ".Str::limit($legacy->title ?? '', 40).'... ');
+
+ try {
+ // Resolve media_task_assignment_id
+ // Chain: legacy.media_order → cooperation_id → task_assignment → media_task_assignment (by partner_media_id)
+ $cooperationId = $this->mediaOrderToCooperation[$legacy->media_order] ?? null;
+
+ if (! $cooperationId) {
+ $this->output->writeln('SKIP (cooperation not found for media_order #'.$legacy->media_order.')>');
+
+ return;
+ }
+
+ $taskAssignment = DB::table('task_assignments')
+ ->where('cooperation_id', $cooperationId)
+ ->select('id')
+ ->first();
+
+ if (! $taskAssignment) {
+ $this->output->writeln('SKIP (task_assignment not found for cooperation #'.$cooperationId.')>');
+
+ return;
+ }
+
+ $mediaTaskAssignment = DB::table('media_task_assignment')
+ ->where('task_assignment_id', $taskAssignment->id)
+ ->where('partner_media_id', $legacy->media)
+ ->select('id')
+ ->first();
+
+ if (! $mediaTaskAssignment) {
+ $this->output->writeln('SKIP (media_task_assignment not found for media #'.$legacy->media.')>');
+
+ return;
+ }
+
+ // Insert the report
+ DB::table('reports')->insert([
+ 'media_task_assignment_id' => $mediaTaskAssignment->id,
+ 'title' => $legacy->title ?? '',
+ 'publication_date' => $legacy->broadcast,
+ 'link' => $legacy->link ?? '',
+ 'description' => '',
+ 'status' => $this->mapApprovalStatus((string) $legacy->status),
+ 'created_at' => $legacy->created_at ?? now(),
+ 'updated_at' => $legacy->updated_at ?? now(),
+ ]);
+
+ $newReportId = (int) DB::getPdo()->lastInsertId();
+
+ // 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('DONE');
+ } catch (\Exception $e) {
+ $this->output->writeln('FAILED: '.$e->getMessage().'');
+ }
+ }
+
+ /**
+ * Migrate the proof image file from S3 to the Report media library.
+ */
+ private function migrateProof(Report $report, object $legacy): void
+ {
+ $legacyPath = trim($legacy->proof ?? '');
+ if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
+ return;
+ }
+
+ $sourcePath = 'old-data/reports/'.$legacyPath;
+ $this->importProofFile($report, $sourcePath, $legacy);
+ }
+
+ /**
+ * Import a single proof image to the Report media library.
+ */
+ private function importProofFile(Report $report, string $sourcePath, object $legacy): void
+ {
+ $attempts = 0;
+ $maxAttempts = 3;
+ $exists = false;
+
+ while ($attempts < $maxAttempts) {
+ try {
+ $exists = Storage::disk('s3')->exists($sourcePath);
+ break;
+ } catch (\Exception $e) {
+ $attempts++;
+ usleep(200000);
+ }
+ }
+
+ if (! $exists) {
+ return;
+ }
+
+ try {
+ $report->addMediaFromDisk($sourcePath, 's3')
+ ->withCustomProperties([
+ 'feature' => 'reports',
+ 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
+ 'doc_type' => 'proof',
+ ])
+ ->preservingOriginal()
+ ->toMediaCollection('reports', 's3');
+ } catch (\Exception $e) {
+ // Log quietly or handle error
+ }
+ }
+
+ /**
+ * Map legacy reports.status integer to ApprovalStatus enum value.
+ * Legacy: 0 = menunggu, 1 = menerima, 2 = menolak
+ */
+ private function mapApprovalStatus(string $legacyStatus): int
+ {
+ return match ($legacyStatus) {
+ '0' => ApprovalStatus::PENDING->value,
+ '1' => ApprovalStatus::ACCEPTED->value,
+ '2' => ApprovalStatus::REJECTED->value,
+ default => ApprovalStatus::PENDING->value,
+ };
+ }
+}