ensurePermission(); $templates = DocumentTemplate::query()->orderByDesc('updated_at')->get(); $actions = Action::with(['decisions:id,name'])->orderBy('name')->get(['id', 'name']); $actionsMapped = $actions->map(fn ($a) => [ 'id' => $a->id, 'name' => $a->name, 'decisions' => $a->decisions->map(fn ($d) => ['id' => $d->id, 'name' => $d->name])->values(), ]); return Inertia::render('Admin/DocumentTemplates/Index', [ 'templates' => $templates, 'actions' => $actionsMapped, ]); } public function toggleActive(DocumentTemplate $template) { $this->ensurePermission(); $template->active = ! $template->active; $template->updated_by = Auth::id(); $template->save(); return redirect()->back()->with('success', 'Status predloge posodobljen.'); } public function show(DocumentTemplate $template) { $this->ensurePermission(); return Inertia::render('Admin/DocumentTemplates/Show', [ 'template' => $template, ]); } public function edit(DocumentTemplate $template) { $this->ensurePermission(); $actions = Action::with(['decisions:id,name'])->orderBy('name')->get(['id', 'name']); $actionsMapped = $actions->map(fn ($a) => [ 'id' => $a->id, 'name' => $a->name, 'decisions' => $a->decisions->map(fn ($d) => ['id' => $d->id, 'name' => $d->name])->values(), ]); return Inertia::render('Admin/DocumentTemplates/Edit', [ 'template' => $template, 'actions' => $actionsMapped, ]); } public function updateSettings(UpdateDocumentTemplateRequest $request, DocumentTemplate $template) { $this->ensurePermission(); $template->fill($request->only([ 'output_filename_pattern', 'date_format', 'action_id', 'decision_id', 'activity_note_template', ])); // If both action & decision provided, ensure decision belongs to action (parity with import templates) if ($request->filled('action_id') && $request->filled('decision_id')) { $belongs = \DB::table('action_decision') ->where('action_id', $request->integer('action_id')) ->where('decision_id', $request->integer('decision_id')) ->exists(); if (! $belongs) { return redirect()->back()->withErrors(['decision_id' => 'Izbrana odločitev ne pripada izbrani akciji.']); } } elseif ($request->filled('action_id') && ! $request->filled('decision_id')) { // Allow clearing decision when action changes if ($template->isDirty('action_id')) { $template->decision_id = null; } } if ($request->has('fail_on_unresolved')) { $template->fail_on_unresolved = (bool) $request->boolean('fail_on_unresolved'); } // Build formatting options array from discrete fields if provided $fmt = $template->formatting_options ?? []; $dirty = false; foreach ([ 'number_decimals', 'decimal_separator', 'thousands_separator', 'currency_symbol', 'currency_position', ] as $key) { if ($request->filled($key)) { $fmt[$key] = $request->input($key); $dirty = true; } elseif ($request->has($key) && $request->input($key) === null) { unset($fmt[$key]); $dirty = true; } } if ($request->has('currency_space')) { $fmt['currency_space'] = (bool) $request->boolean('currency_space'); $dirty = true; } if ($request->filled('default_date_format')) { $fmt['default_date_format'] = $request->input('default_date_format'); $dirty = true; } if ($request->has('date_formats')) { $fmt['date_formats'] = array_filter((array) $request->input('date_formats'), fn ($v) => $v !== null && $v !== ''); $dirty = true; } if ($dirty) { $template->formatting_options = $fmt; } // Merge meta, including custom_defaults if ($request->has('meta') && is_array($request->input('meta'))) { $meta = array_filter($request->input('meta'), fn ($v) => $v !== null && $v !== ''); $template->meta = array_replace($template->meta ?? [], $meta); } $template->updated_by = Auth::id(); $template->save(); return redirect()->back()->with('success', 'Nastavitve predloge shranjene.'); } public function store(StoreDocumentTemplateRequest $request) { $this->ensurePermission(); $file = $request->file('file'); // Basic extension guard (defense in depth vs only MIME detection) if (strtolower($file->getClientOriginalExtension()) !== 'docx') { return redirect()->back()->withErrors(['file' => 'Datoteka mora biti DOCX.']); } $slug = Str::slug($request->slug); // Determine next version if slug exists $latest = DocumentTemplate::where('slug', $slug)->orderByDesc('version')->first(); $nextVersion = $latest ? ($latest->version + 1) : 1; $hash = hash_file('sha256', $file->getRealPath()); $path = $file->store("document-templates/{$slug}/v{$nextVersion}", 'public'); // Scan tokens from uploaded DOCX (best effort) $tokens = []; try { /** @var TokenScanner $scanner */ $scanner = app(TokenScanner::class); $zip = new \ZipArchive; $tmp = tempnam(sys_get_temp_dir(), 'tmpl'); copy($file->getRealPath(), $tmp); if ($zip->open($tmp) === true) { $xml = $zip->getFromName('word/document.xml'); if ($xml !== false) { $tokens = $scanner->scan($xml); } $zip->close(); } } catch (\Throwable $e) { // swallow scanning errors } // (Future) Could refine allowed columns automatically based on tokens $entities = ['contract', 'client_case', 'client', 'person', 'account']; $columns = [ 'contract' => ['reference', 'start_date', 'end_date', 'description'], 'client_case' => ['client_ref'], 'client' => [], 'person' => ['full_name', 'first_name', 'last_name', 'nu'], // Add common account attributes; whitelist may further refine 'account' => ['reference', 'initial_amount', 'balance_amount', 'promise_date'], ]; $payload = [ 'name' => $request->name, 'slug' => $slug, 'custom_name' => $request->custom_name, 'description' => $request->description, 'core_entity' => 'contract', 'entities' => $entities, 'columns' => $columns, 'version' => $nextVersion, 'engine' => 'tokens', 'file_path' => $path, 'file_hash' => $hash, 'file_size' => $file->getSize(), 'mime_type' => $file->getMimeType(), 'active' => true, 'created_by' => $latest ? $latest->created_by : Auth::id(), // preserve original author for lineage if re-upload 'updated_by' => Auth::id(), 'formatting_options' => [ 'number_decimals' => 2, 'decimal_separator' => ',', 'thousands_separator' => '.', 'currency_symbol' => '€', 'currency_position' => 'after', 'currency_space' => true, ], ]; // Optional meta + activity linkage fields (parity with import templates style) if ($request->filled('meta') && is_array($request->input('meta'))) { $payload['meta'] = array_filter($request->input('meta'), fn ($v) => $v !== null && $v !== ''); } if ($request->filled('action_id')) { $payload['action_id'] = $request->integer('action_id'); } if ($request->filled('decision_id')) { $payload['decision_id'] = $request->integer('decision_id'); } if ($request->filled('activity_note_template')) { $payload['activity_note_template'] = $request->input('activity_note_template'); } if (Schema::hasColumn('document_templates', 'tokens')) { $payload['tokens'] = $tokens; } $template = DocumentTemplate::create($payload); return redirect()->back()->with('success', 'Predloga uspešno shranjena. (v'.$template->version.')')->with('template_id', $template->id); } private function ensurePermission(): void { if (Gate::denies('manage-document-templates') && Gate::denies('manage-settings')) { abort(403); } } }