Teren-app/app/Http/Controllers/Admin/DocumentTemplateController.php
Simon Pocrnjič e0303ece74 documents
2025-10-12 12:24:17 +02:00

239 lines
9.4 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreDocumentTemplateRequest;
use App\Http\Requests\UpdateDocumentTemplateRequest;
use App\Models\Action;
use App\Models\DocumentTemplate;
use App\Services\Documents\TokenScanner;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Inertia\Inertia;
class DocumentTemplateController extends Controller
{
public function index()
{
$this->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);
}
}
}