33namespace App \Http \Controllers \Web \Admin ;
44
55use App \Http \Controllers \Controller ;
6+ use App \Http \Requests \InspectionPlanRequest ;
67use App \Models \InspectionPlan ;
78use App \Models \Material ;
89use App \Models \MaterialType ;
9- use Illuminate \ Http \ Request ;
10+ use App \ Services \ Quality \ InspectionPlanVersionService ;
1011use Inertia \Inertia ;
1112
1213class InspectionPlanController extends Controller
1314{
15+ public function __construct (private InspectionPlanVersionService $ versions ) {}
16+
1417 public function index ()
1518 {
1619 return Inertia::render ('admin/inspection-plans/Index ' , [
@@ -32,82 +35,95 @@ public function create()
3235 return Inertia::render ('admin/inspection-plans/Create ' , $ this ->formData ());
3336 }
3437
35- public function store (Request $ request )
38+ /**
39+ * Create a brand-new plan as version 1 — a draft until published.
40+ */
41+ public function store (InspectionPlanRequest $ request )
3642 {
37- $ validated = $ this ->validated ($ request );
38- InspectionPlan::create ($ validated );
43+ InspectionPlan::create ([
44+ ...$ request ->payload (),
45+ 'version ' => 1 ,
46+ 'published_at ' => null ,
47+ 'root_id ' => null ,
48+ 'is_active ' => false ,
49+ ]);
3950
40- return redirect ()->route ('admin.inspection-plans.index ' )->with ('success ' , __ ('Inspection plan created. ' ));
51+ return redirect ()->route ('admin.inspection-plans.index ' )
52+ ->with ('success ' , __ ('Inspection plan created as a draft. Publish it to use it for inspections. ' ));
4153 }
4254
4355 public function edit (InspectionPlan $ inspectionPlan )
4456 {
45- // Derive scope from which target FK is set (scope isn't a stored column).
46- $ scope = $ inspectionPlan ->material_id ? 'material ' : ($ inspectionPlan ->material_type_id ? 'material_type ' : 'generic ' );
57+ $ scope = $ inspectionPlan ->material_id ? 'material '
58+ : ($ inspectionPlan ->material_type_id ? 'material_type ' : 'generic ' );
59+
60+ $ history = $ inspectionPlan ->versionGroup ()
61+ ->get (['id ' , 'version ' , 'published_at ' , 'is_active ' , 'updated_at ' ])
62+ ->map (fn ($ v ) => [
63+ 'id ' => $ v ->id ,
64+ 'version ' => $ v ->version ,
65+ 'is_draft ' => $ v ->published_at === null ,
66+ 'is_active ' => (bool ) $ v ->is_active ,
67+ 'published_at ' => $ v ->published_at ?->toIso8601String(),
68+ 'updated_at ' => $ v ->updated_at ?->toIso8601String(),
69+ ]);
4770
4871 return Inertia::render ('admin/inspection-plans/Edit ' , array_merge ($ this ->formData (), [
4972 'plan ' => [
50- ...$ inspectionPlan ->only ('id ' , 'name ' , 'description ' , 'material_id ' , 'material_type_id ' , 'criteria ' , 'is_active ' ),
73+ ...$ inspectionPlan ->only ('id ' , 'name ' , 'description ' , 'material_id ' , 'material_type_id ' , 'criteria ' , 'is_active ' , ' version ' ),
5174 'scope ' => $ scope ,
75+ 'is_draft ' => $ inspectionPlan ->isDraft (),
76+ 'published_at ' => $ inspectionPlan ->published_at ?->toIso8601String(),
5277 ],
78+ 'history ' => $ history ,
5379 ]));
5480 }
5581
56- public function update (Request $ request , InspectionPlan $ inspectionPlan )
82+ /**
83+ * Draft → edit in place. Published → spawn the next draft version
84+ * (the published version stays immutable for reproducibility).
85+ */
86+ public function update (InspectionPlanRequest $ request , InspectionPlan $ inspectionPlan )
5787 {
58- $ inspectionPlan ->update ($ this ->validated ($ request ));
88+ if ($ inspectionPlan ->isDraft ()) {
89+ $ inspectionPlan ->update ($ request ->payload ());
5990
60- return redirect ()->route ('admin.inspection-plans.index ' )->with ('success ' , __ ('Inspection plan updated. ' ));
61- }
91+ return redirect ()->route ('admin.inspection-plans.index ' )
92+ ->with ('success ' , __ ('Draft updated. ' ));
93+ }
6294
63- public function destroy (InspectionPlan $ inspectionPlan )
64- {
65- $ inspectionPlan ->delete ();
95+ $ newVersion = $ this ->versions ->createNewVersion ($ inspectionPlan , $ request ->payload ());
6696
67- return redirect ()->route ('admin.inspection-plans.index ' )->with ('success ' , __ ('Inspection plan deleted. ' ));
97+ return redirect ()->route ('admin.inspection-plans.edit ' , $ newVersion )
98+ ->with ('success ' , __ ('Created version :v as a draft from the published plan. ' , ['v ' => $ newVersion ->version ]));
6899 }
69100
70- private function validated (Request $ request ): array
101+ /**
102+ * Publish a draft — makes it the live version and retires the previous one.
103+ */
104+ public function publish (InspectionPlan $ inspectionPlan )
71105 {
72- $ validated = $ request ->validate ([
73- 'name ' => 'required|string|max:150 ' ,
74- 'description ' => 'nullable|string ' ,
75- 'scope ' => 'required|string|in:material,material_type,generic ' ,
76- 'material_id ' => 'nullable|integer|exists:materials,id ' ,
77- 'material_type_id ' => 'nullable|integer|exists:material_types,id ' ,
78- 'criteria ' => 'required|array|min:1 ' ,
79- 'criteria.*.name ' => 'required|string|max:150 ' ,
80- 'criteria.*.type ' => 'required|string|in:visual,measurement,functional,pass_fail ' ,
81- 'criteria.*.required ' => 'nullable|boolean ' ,
82- 'criteria.*.unit ' => 'nullable|string|max:30 ' ,
83- 'criteria.*.spec_min ' => 'nullable|numeric ' ,
84- 'criteria.*.spec_max ' => 'nullable|numeric ' ,
85- 'is_active ' => 'nullable|boolean ' ,
86- ]);
87-
88- // Enforce scope coherence based on the radio choice.
89- $ scope = $ validated ['scope ' ];
90- if ($ scope === 'material ' ) {
91- abort_unless ($ validated ['material_id ' ] ?? null , 422 , 'Pick a material when scope = material. ' );
92- $ validated ['material_type_id ' ] = null ;
93- } elseif ($ scope === 'material_type ' ) {
94- abort_unless ($ validated ['material_type_id ' ] ?? null , 422 , 'Pick a material type when scope = material_type. ' );
95- $ validated ['material_id ' ] = null ;
96- } else {
97- $ validated ['material_id ' ] = null ;
98- $ validated ['material_type_id ' ] = null ;
106+ if ($ inspectionPlan ->isPublished ()) {
107+ return back ()->with ('error ' , __ ('This version is already published. ' ));
99108 }
100109
101- unset($ validated ['scope ' ]);
102- $ validated ['is_active ' ] = $ validated ['is_active ' ] ?? false ;
110+ $ this ->versions ->publish ($ inspectionPlan );
103111
104- // Normalize criteria booleans (HTML form sends nothing when unchecked).
105- $ validated [ ' criteria ' ] = array_map ( function ( $ c ) {
106- $ c [ ' required ' ] = isset ( $ c [ ' required ' ]) ? ( bool ) $ c [ ' required ' ] : false ;
112+ return redirect ()-> route ( ' admin.inspection-plans.index ' )
113+ -> with ( ' success ' , __ ( ' Inspection plan version :v published. ' , [ ' v ' => $ inspectionPlan -> version ]));
114+ }
107115
108- return $ c ;
109- }, $ validated ['criteria ' ]);
116+ public function destroy (InspectionPlan $ inspectionPlan )
117+ {
118+ // Published versions that have been used by inspections must stay for
119+ // historical reproducibility.
120+ if ($ inspectionPlan ->isPublished () && $ inspectionPlan ->inspections ()->exists ()) {
121+ return back ()->with ('error ' , __ ('Cannot delete a published version that has recorded inspections. ' ));
122+ }
123+
124+ $ inspectionPlan ->delete ();
110125
111- return $ validated ;
126+ return redirect ()->route ('admin.inspection-plans.index ' )
127+ ->with ('success ' , __ ('Inspection plan deleted. ' ));
112128 }
113129}
0 commit comments