ClassPackageService.php 6.2 KB

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