ManualController.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. /*
  3. * ManualController — управление справочниками (разделы и значения).
  4. *
  5. * Создан: 2026-05-06
  6. * Маршруты: /admin/manuals/... (не resource, ручные маршруты в routes/web.php)
  7. * Защита: middleware 'admin' (EnsureUserIsAdmin)
  8. *
  9. * Справочники — двухуровневая структура:
  10. * DictSection (разделы): например "Марки и модели", "Типы кузова"
  11. * DictValue (значения): Toyota → Camry, Corolla и т.д.
  12. * Поддерживает иерархические (is_hierarchical=true) и плоские (false) разделы.
  13. */
  14. namespace App\Http\Controllers\Admin;
  15. use App\Http\Controllers\Controller;
  16. use App\Models\DictSection;
  17. use App\Models\DictValue;
  18. use Illuminate\Http\Request;
  19. use Illuminate\Support\Facades\Storage;
  20. class ManualController extends Controller
  21. {
  22. // ── Разделы справочников ────────────────────────────────────
  23. // Список всех разделов справочников, отсортированных по sort_order
  24. // Вьюха: resources/views/admin/manuals/index.blade.php
  25. public function index()
  26. {
  27. $sections = DictSection::orderBy('sort_order')->get();
  28. return view('admin.manuals.index', compact('sections'));
  29. }
  30. // Форма создания нового раздела справочника
  31. // Вьюха: resources/views/admin/manuals/section_form.blade.php
  32. public function sectionCreate()
  33. {
  34. return view('admin.manuals.section_form');
  35. }
  36. // Сохранение нового раздела; is_system всегда false — системные разделы создаются только через сидер
  37. public function sectionStore(Request $request)
  38. {
  39. $request->validate([
  40. 'code' => 'required|string|max:64|unique:dict_sections,code',
  41. 'label' => 'required|string|max:128',
  42. 'is_hierarchical' => 'nullable|boolean',
  43. 'sort_order' => 'nullable|integer',
  44. ]);
  45. DictSection::create([
  46. 'code' => $request->code,
  47. 'label' => $request->label,
  48. 'is_system' => false,
  49. 'is_hierarchical' => (bool) $request->input('is_hierarchical', false),
  50. 'sort_order' => (int) $request->input('sort_order', 0),
  51. ]);
  52. return redirect()->route('admin.manuals.index')->with('success', 'Раздел добавлен.');
  53. }
  54. // Форма редактирования раздела справочника (та же вьюха, что и sectionCreate)
  55. public function sectionEdit(DictSection $section)
  56. {
  57. return view('admin.manuals.section_form', compact('section'));
  58. }
  59. // Обновление раздела; unique-правило исключает текущую запись: unique:dict_sections,code,{id}
  60. public function sectionUpdate(Request $request, DictSection $section)
  61. {
  62. $request->validate([
  63. 'code' => 'required|string|max:64|unique:dict_sections,code,'.$section->id,
  64. 'label' => 'required|string|max:128',
  65. 'is_hierarchical' => 'nullable|boolean',
  66. 'sort_order' => 'nullable|integer',
  67. ]);
  68. $section->update([
  69. 'code' => $request->code,
  70. 'label' => $request->label,
  71. 'is_hierarchical' => (bool) $request->input('is_hierarchical', false),
  72. 'sort_order' => (int) $request->input('sort_order', 0),
  73. ]);
  74. return redirect()->route('admin.manuals.index')->with('success', 'Раздел обновлён.');
  75. }
  76. // Удаление раздела; системные (is_system=true) удалять нельзя — защита от разрушения структуры
  77. // При удалении раздела cascade удаляет все его значения (настроено в миграции FK)
  78. public function sectionDestroy(DictSection $section)
  79. {
  80. if ($section->is_system) {
  81. return back()->with('error', 'Системный раздел нельзя удалить.');
  82. }
  83. $section->delete();
  84. return redirect()->route('admin.manuals.index')->with('success', 'Раздел удалён.');
  85. }
  86. // ── Значения справочника ────────────────────────────────────
  87. // Список значений раздела; иерархические — с children (eager load); плоские — простой список
  88. // Вьюха: resources/views/admin/manuals/values.blade.php (показывает оба варианта через @if)
  89. public function values(DictSection $section)
  90. {
  91. if ($section->is_hierarchical) {
  92. $items = DictValue::where('section_id', $section->id)
  93. ->whereNull('parent_id')
  94. ->with('children')
  95. ->orderBy('sort_order')
  96. ->get();
  97. } else {
  98. $items = DictValue::where('section_id', $section->id)
  99. ->orderBy('sort_order')
  100. ->get();
  101. }
  102. return view('admin.manuals.values', compact('section', 'items'));
  103. }
  104. // Форма добавления значения; если передан parent_id в URL → предзаполняет родителя
  105. // Вьюха: resources/views/admin/manuals/value_form.blade.php
  106. public function valueCreate(DictSection $section, Request $request)
  107. {
  108. $parent = null;
  109. if ($request->filled('parent_id')) {
  110. $parent = DictValue::findOrFail($request->parent_id);
  111. }
  112. return view('admin.manuals.value_form', compact('section', 'parent'));
  113. }
  114. // Сохранение нового значения; parent_id = null для верхнего уровня иерархии
  115. public function valueStore(DictSection $section, Request $request)
  116. {
  117. $request->validate([
  118. 'value' => 'required|string|max:255',
  119. 'flag' => 'nullable|string|max:10',
  120. 'parent_id' => 'nullable|integer|exists:dict_values,id',
  121. 'sort_order' => 'nullable|integer',
  122. 'logo' => 'nullable|image|max:2048',
  123. ]);
  124. $logoPath = null;
  125. if ($request->hasFile('logo') && $request->file('logo')->isValid()) {
  126. $logoPath = $request->file('logo')->store('makes/logos', 'public');
  127. }
  128. DictValue::create([
  129. 'section_id' => $section->id,
  130. 'parent_id' => $request->input('parent_id') ?: null,
  131. 'value' => $request->value,
  132. 'flag' => $request->input('flag', ''),
  133. 'logo' => $logoPath,
  134. 'sort_order' => (int) $request->input('sort_order', 0),
  135. ]);
  136. return redirect()->route('admin.manuals.values', $section)->with('success', 'Значение добавлено.');
  137. }
  138. // Форма редактирования значения; загружает текущего родителя для отображения в форме
  139. public function valueEdit(DictSection $section, DictValue $value)
  140. {
  141. $parent = $value->parent_id ? DictValue::find($value->parent_id) : null;
  142. return view('admin.manuals.value_form', compact('section', 'value', 'parent'));
  143. }
  144. // Обновление значения; parent_id обнуляется если пустой (ternary: '' → null)
  145. public function valueUpdate(Request $request, DictSection $section, DictValue $value)
  146. {
  147. $request->validate([
  148. 'value' => 'required|string|max:255',
  149. 'flag' => 'nullable|string|max:10',
  150. 'parent_id' => 'nullable|integer|exists:dict_values,id',
  151. 'sort_order' => 'nullable|integer',
  152. 'logo' => 'nullable|image|max:2048',
  153. 'remove_logo' => 'nullable|boolean',
  154. ]);
  155. $data = [
  156. 'parent_id' => $request->input('parent_id') ?: null,
  157. 'value' => $request->value,
  158. 'flag' => $request->input('flag', ''),
  159. 'sort_order' => (int) $request->input('sort_order', 0),
  160. ];
  161. if ($request->hasFile('logo') && $request->file('logo')->isValid()) {
  162. // Удаляем старый логотип перед заменой
  163. if ($value->logo) {
  164. Storage::disk('public')->delete($value->logo);
  165. }
  166. $data['logo'] = $request->file('logo')->store('makes/logos', 'public');
  167. } elseif ($request->boolean('remove_logo') && $value->logo) {
  168. Storage::disk('public')->delete($value->logo);
  169. $data['logo'] = null;
  170. }
  171. $value->update($data);
  172. return redirect()->route('admin.manuals.values', $section)->with('success', 'Значение обновлено.');
  173. }
  174. // Удаление значения; cascade из FK удалит и все дочерние значения (если иерархия)
  175. public function valueDestroy(DictSection $section, DictValue $value)
  176. {
  177. $value->delete();
  178. return redirect()->route('admin.manuals.values', $section)->with('success', 'Значение удалено.');
  179. }
  180. }