ClassPackageService.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. namespace App\Services;
  3. use App\Models\ClassPackage;
  4. use App\Models\ClassPackageUnit;
  5. use App\Models\IrrecusableProposal;
  6. use App\Models\IrrecusableProposalProduct;
  7. use App\Models\PavaoProposal;
  8. use App\Models\PavaoProposalProduct;
  9. use App\Models\Unit;
  10. use Illuminate\Database\Eloquent\Collection;
  11. use Illuminate\Support\Facades\DB;
  12. class ClassPackageService
  13. {
  14. public function __construct(
  15. protected ClassPackageUnitService $unitService,
  16. protected ProposalPricingService $pricingService,
  17. ) {}
  18. public function getAll(): Collection
  19. {
  20. return ClassPackage::with('products')->orderBy('created_at', 'desc')->get();
  21. }
  22. public function findById(int $id): ?ClassPackage
  23. {
  24. return ClassPackage::with(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product'])->find($id);
  25. }
  26. public function create(array $data): ClassPackage
  27. {
  28. $materials = $data['materials'] ?? [];
  29. $unitVisibilities = $data['unit_visibilities'] ?? null;
  30. $groupIds = $data['group_ids'] ?? [];
  31. $pavao = $data['pavao'] ?? [];
  32. $irrecusavel = $data['irrecusavel'] ?? [];
  33. unset($data['materials'], $data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
  34. $data['contract_material_value'] = $this->calcMaterialValue($materials);
  35. $package = ClassPackage::create($data);
  36. $this->syncMaterials($package, $materials);
  37. $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
  38. $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
  39. // Replicate to all units (all start visible = true)
  40. $this->replicateToAllUnits($package->load(['products', 'pavaoProposal.products', 'irrecusableProposal.products']));
  41. // Sync selected groups
  42. if (!empty($groupIds)) {
  43. $package->groups()->sync($groupIds);
  44. }
  45. // Apply visibility overrides
  46. $this->applyVisibilityOverrides($package, $unitVisibilities, $groupIds);
  47. return $package->load(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product']);
  48. }
  49. public function update(int $id, array $data): ?ClassPackage
  50. {
  51. $package = $this->findById($id);
  52. if (!$package) return null;
  53. $materials = $data['materials'] ?? null;
  54. $unitVisibilities = $data['unit_visibilities'] ?? null;
  55. $groupIds = array_key_exists('group_ids', $data) ? ($data['group_ids'] ?? []) : null;
  56. $pavao = $data['pavao'] ?? null;
  57. $irrecusavel = $data['irrecusavel'] ?? null;
  58. unset($data['materials'], $data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
  59. if ($materials !== null) {
  60. $data['contract_material_value'] = $this->calcMaterialValue($materials);
  61. }
  62. $package->update($data);
  63. $scheduleFields = array_intersect_key($data, array_flip([
  64. 'class_duration_minutes',
  65. 'weekday',
  66. 'start_time',
  67. 'second_weekday',
  68. 'second_start_time',
  69. ]));
  70. if (!empty($scheduleFields)) {
  71. ClassPackageUnit::where('class_package_id', $package->id)->update($scheduleFields);
  72. }
  73. if ($materials !== null) {
  74. $this->syncMaterials($package, $materials);
  75. // Propagate material changes to unit packages
  76. $this->syncUnitPackageProducts($package->load('products'));
  77. }
  78. // Pavão/Irrecusável updates never propagate to already-cloned unit copies.
  79. if ($pavao !== null) {
  80. $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
  81. }
  82. if ($irrecusavel !== null) {
  83. $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
  84. }
  85. if ($groupIds !== null) {
  86. $package->groups()->sync($groupIds);
  87. }
  88. // Determine current group IDs (after sync)
  89. $currentGroupIds = $package->groups()->pluck('groups.id')->toArray();
  90. if ($unitVisibilities !== null || $groupIds !== null) {
  91. $this->applyVisibilityOverrides($package, $unitVisibilities, $currentGroupIds);
  92. }
  93. return $package->fresh(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product']);
  94. }
  95. public function delete(int $id): bool
  96. {
  97. $package = ClassPackage::find($id);
  98. if (!$package) return false;
  99. return $package->delete();
  100. }
  101. public function replicateToAllUnits(ClassPackage $package): void
  102. {
  103. Unit::all()->each(function (Unit $unit) use ($package) {
  104. // Skip if ClassPackageUnit already exists for this package/unit combo
  105. $exists = ClassPackageUnit::withTrashed()
  106. ->where('class_package_id', $package->id)
  107. ->where('unit_id', $unit->id)
  108. ->exists();
  109. if (!$exists) {
  110. $this->unitService->replicateFromBasePackage($unit->id, $package);
  111. }
  112. });
  113. }
  114. /**
  115. * Apply visibility overrides:
  116. * - Units in selected groups → forced visible = true
  117. * - Other units → use explicit unit_visibilities (if provided)
  118. */
  119. private function applyVisibilityOverrides(ClassPackage $package, ?array $unitVisibilities, array $groupIds): void
  120. {
  121. // Get all unit IDs that belong to the selected groups
  122. $groupUnitIds = [];
  123. if (!empty($groupIds)) {
  124. $groupUnitIds = DB::table('group_units')
  125. ->whereIn('group_id', $groupIds)
  126. ->pluck('unit_id')
  127. ->unique()
  128. ->toArray();
  129. }
  130. // Force group units to visible = true
  131. if (!empty($groupUnitIds)) {
  132. ClassPackageUnit::where('class_package_id', $package->id)
  133. ->whereIn('unit_id', $groupUnitIds)
  134. ->update(['visible' => true]);
  135. }
  136. // Apply explicit visibility for non-group units
  137. if (!empty($unitVisibilities)) {
  138. foreach ($unitVisibilities as $uv) {
  139. if (in_array($uv['unit_id'], $groupUnitIds)) {
  140. continue; // group units cannot be hidden
  141. }
  142. ClassPackageUnit::where('class_package_id', $package->id)
  143. ->where('unit_id', $uv['unit_id'])
  144. ->update(['visible' => $uv['visible']]);
  145. }
  146. }
  147. }
  148. /**
  149. * When materials are updated, propagate product changes to all ClassPackageUnit records.
  150. */
  151. private function syncUnitPackageProducts(ClassPackage $package): void
  152. {
  153. ClassPackageUnit::where('class_package_id', $package->id)->each(function (ClassPackageUnit $up) use ($package) {
  154. $this->unitService->syncProductsFromBasePackage($up, $package);
  155. });
  156. }
  157. private function syncMaterials(ClassPackage $package, array $materials): void
  158. {
  159. $syncData = [];
  160. foreach ($materials as $m) {
  161. $syncData[$m['product_id']] = [
  162. 'quantity' => $m['quantity'],
  163. 'price' => $m['price'],
  164. ];
  165. }
  166. $package->products()->sync($syncData);
  167. }
  168. private function calcMaterialValue(array $materials): float
  169. {
  170. return array_reduce($materials, fn($carry, $m) => $carry + ($m['quantity'] * $m['price']), 0.0);
  171. }
  172. }