middleware(['admin', '2fa']); } // ── Permissions: offers-r = Routing (Lesen), fein: offers-w, offers-send, offers-accept public function hasOfferPermission(string $key): bool { $user = auth()->user(); if (! $user) { return false; } return $user->isPermission($key); } public function assertOfferPermission(string $key): void { if (! $this->hasOfferPermission($key)) { abort(Response::HTTP_FORBIDDEN, 'Fehlende Berechtigung: ' . $key); } } public function index($step = false) { return view('offer.index', [ 'step' => $step, ]); } public function getOffers() { $query = Offer::query() ->with(['createdBy', 'contact']) ->select('offers.*'); return \DataTables::eloquent($query) ->addColumn('action_edit', function (Offer $offer) { return '' . ''; }) ->addColumn('id', function (Offer $offer) { return '' . (int) $offer->id . ''; }) ->addColumn('offer_number', function (Offer $offer) { return e($offer->offer_number); }) ->addColumn('contact_name', function (Offer $offer) { $c = $offer->contact; if (! $c) { return ''; } return e(trim(($c->firstname ?? '') . ' ' . ($c->name ?? ''))); }) ->addColumn('status_badge', function (Offer $offer) { return '' . e($offer->status) . ''; }) ->addColumn('created_name', function (Offer $offer) { return e($offer->createdBy->name ?? ''); }) ->addColumn('created_at_fmt', function (Offer $offer) { if (! $offer->created_at) { return ''; } return e($offer->created_at->format(\Util::formatDateTimeDB())); }) ->orderColumn('id', 'offers.id $1') ->orderColumn('offer_number', 'offers.offer_number $1') ->rawColumns(['action_edit', 'id', 'status_badge']) ->make(true); } public function detail($id) { if ($id === 'new') { abort(404, 'Anlage über Modal folgt in B2.'); } $offer = Offer::query() ->with(['currentVersion.items', 'contact', 'inquiry', 'createdBy']) ->findOrFail($id); return view('offer.detail', [ 'offer' => $offer, 'id' => $offer->id, ]); } public function store(Request $request, $id) { $this->assertOfferPermission('offers-w'); if (! $request->has('action')) { abort(403, 'keine Action'); } // B3: Angebots-Editor, Autosave, … \Session::flash('alert-info', 'Speichern (Stub A6) — B3 liefert die Logik.'); return redirect()->to(route('offer_detail', [$id]) . (string) $request->get('fragment', '')); } public function loadModal(Request $request) { $data = $request->all(); $html = ''; if (($data['action'] ?? null) === 'modal-new-offer') { $html = view('offer.modal_new_offer_stub', ['data' => $data])->render(); } if ($request->ajax() || $request->wantsJson()) { return response()->json(['html' => $html, 'response' => $data]); } return response($html); } public function action(Request $request, $action, $id = null) { $wActions = [ 'create', 'update', 'supersede', 'duplicate', 'delete-file', ]; $sendActions = ['send', 'resend']; $acceptActions = ['markAccepted', 'markDeclined', 'withdraw', 'mark_accepted', 'mark_declined']; if (in_array($action, $wActions, true)) { $this->assertOfferPermission('offers-w'); } if (in_array($action, $sendActions, true)) { $this->assertOfferPermission('offers-send'); } if (in_array($action, $acceptActions, true)) { $this->assertOfferPermission('offers-accept'); } $any = array_merge($wActions, $sendActions, $acceptActions); if (! in_array($action, $any, true)) { $this->assertOfferPermission('offers-w'); } if ($id === null) { abort(404); } if ($id !== 'new') { $offer = Offer::find($id); if (! $offer) { abort(404); } } else { $offer = null; } \Session::flash('alert-info', 'Aktion «' . e($action) . '» (Stub A6) — siehe B2/B3.'); if ($offer) { return redirect()->route('offer_detail', [$offer->id]); } return redirect()->route('offers'); } public function delete($id, $del = null) { $this->assertOfferPermission('offers-w'); \Session::flash('alert-info', 'Löschen (Stub) — B9 liefert Soft-Delete-Logik.'); return redirect()->route('offers'); } /** * PDF-Generierung: Stub (Ticket B5 liefert den Stream). */ public function pdf($versionId, $do = null) { $version = OfferVersion::query()->with('offer')->findOrFail($versionId); return response('PDF-Generator folgt in B5 (OfferVersion #' . (int) $version->id . ').', 200, [ 'Content-Type' => 'text/plain; charset=UTF-8', ]); } }