Added the support for generating docs from template doc
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Events\DocumentSettingsUpdated;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Documents\DocumentSettings as SettingsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class DocumentSettingsController extends Controller
|
||||
{
|
||||
public function edit(SettingsService $svc)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
$settings = $svc->get();
|
||||
|
||||
return Inertia::render('Admin/DocumentSettings/Edit', [
|
||||
'settings' => $settings,
|
||||
'defaults' => [
|
||||
'file_name_pattern' => config('documents.file_name_pattern'),
|
||||
'date_format' => config('documents.date_format'),
|
||||
'unresolved_policy' => config('documents.unresolved_policy'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, SettingsService $svc)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
$data = $request->validate([
|
||||
'file_name_pattern' => ['required', 'string', 'max:255'],
|
||||
'date_format' => ['required', 'string', 'max:40'],
|
||||
'unresolved_policy' => ['required', 'in:fail,blank,keep'],
|
||||
'preview_enabled' => ['required', 'boolean'],
|
||||
'whitelist' => ['nullable', 'array'],
|
||||
'whitelist.*' => ['array'],
|
||||
'date_formats' => ['nullable', 'array'],
|
||||
'date_formats.*' => ['string'],
|
||||
]);
|
||||
$settings = $svc->get();
|
||||
$settings->fill($data)->save();
|
||||
$svc->refresh();
|
||||
event(new DocumentSettingsUpdated($settings));
|
||||
|
||||
return redirect()->back()->with('success', 'Nastavitve shranjene.');
|
||||
}
|
||||
|
||||
private function authorizeAccess(): void
|
||||
{
|
||||
if (Gate::denies('manage-settings') && Gate::denies('manage-document-templates')) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
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;
|
||||
@@ -19,9 +20,16 @@ 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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,10 +43,51 @@ public function toggleActive(DocumentTemplate $template)
|
||||
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']));
|
||||
$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');
|
||||
}
|
||||
@@ -153,6 +202,19 @@ public function store(StoreDocumentTemplateRequest $request)
|
||||
'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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StorePermissionRequest;
|
||||
use App\Models\Permission;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$permissions = Permission::query()
|
||||
->select('id','name','slug','description','created_at')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return Inertia::render('Admin/Permissions/Index', [
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
}
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Admin/Permissions/Create');
|
||||
}
|
||||
|
||||
public function store(StorePermissionRequest $request): RedirectResponse
|
||||
{
|
||||
Permission::create($request->validated());
|
||||
|
||||
return redirect()->route('admin.index')->with('success', 'Dovoljenje ustvarjeno.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class UserRoleController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
Gate::authorize('manage-settings');
|
||||
|
||||
$users = User::with('roles:id,slug,name')->orderBy('name')->get(['id', 'name', 'email']);
|
||||
$roles = Role::with('permissions:id,slug,name')->orderBy('name')->get(['id', 'name', 'slug']);
|
||||
$permissions = Permission::orderBy('slug')->get(['id', 'name', 'slug']);
|
||||
|
||||
return Inertia::render('Admin/Users/Index', [
|
||||
'users' => $users,
|
||||
'roles' => $roles,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
Gate::authorize('manage-settings');
|
||||
|
||||
$validated = $request->validate([
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer', 'exists:roles,id'],
|
||||
]);
|
||||
|
||||
$user->roles()->sync($validated['roles'] ?? []);
|
||||
|
||||
return back()->with('success', 'Roles updated');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\DocumentGenerated;
|
||||
use App\Models\Activity;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Services\Documents\TokenValueResolver;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ContractDocumentGenerationController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Contract $contract): Response
|
||||
{
|
||||
if (Gate::denies('read')) { // baseline read permission required to generate
|
||||
abort(403);
|
||||
}
|
||||
$request->validate([
|
||||
'template_slug' => ['required', 'string', 'exists:document_templates,slug'],
|
||||
]);
|
||||
|
||||
$template = DocumentTemplate::where('slug', $request->template_slug)
|
||||
->where('core_entity', 'contract')
|
||||
->where('active', true)
|
||||
->orderByDesc('version')
|
||||
->firstOrFail();
|
||||
|
||||
// Load related data minimally
|
||||
$contract->load(['clientCase.client.person', 'clientCase.person', 'clientCase.client']);
|
||||
|
||||
$renderer = app(\App\Services\Documents\DocxTemplateRenderer::class);
|
||||
try {
|
||||
$result = $renderer->render($template, $contract, Auth::user());
|
||||
} catch (\App\Services\Documents\Exceptions\UnresolvedTokensException $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Unresolved tokens detected.',
|
||||
'tokens' => $e->unresolved ?? [],
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Generation failed.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
$doc = new Document;
|
||||
$doc->fill([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => $result['fileName'],
|
||||
'description' => 'Generated from template '.$template->slug.' v'.$template->version,
|
||||
'user_id' => Auth::id(),
|
||||
'disk' => 'public',
|
||||
'path' => $result['relativePath'],
|
||||
'file_name' => $result['fileName'],
|
||||
'original_name' => $result['fileName'],
|
||||
'extension' => 'docx',
|
||||
'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'size' => $result['size'],
|
||||
'checksum' => $result['checksum'],
|
||||
'is_public' => true,
|
||||
'template_id' => $template->id,
|
||||
'template_version' => $template->version,
|
||||
]);
|
||||
$contract->documents()->save($doc);
|
||||
|
||||
// Dispatch domain event
|
||||
event(new DocumentGenerated($doc));
|
||||
|
||||
// Optional: create an activity if template links to action/decision
|
||||
if (($template->action_id || $template->decision_id || $template->activity_note_template) && $contract->client_case_id) {
|
||||
try {
|
||||
$note = null;
|
||||
if ($template->activity_note_template) {
|
||||
// Interpolate tokens in note using existing resolver logic (non-failing policy: keep)
|
||||
/** @var TokenValueResolver $resolver */
|
||||
$resolver = app(TokenValueResolver::class);
|
||||
$rawNote = $template->activity_note_template;
|
||||
$tokens = [];
|
||||
if (preg_match_all('/\{([a-zA-Z0-9_\.]+)\}/', $rawNote, $m)) {
|
||||
$tokens = array_unique($m[1]);
|
||||
}
|
||||
$values = [];
|
||||
if ($tokens) {
|
||||
$resolved = $resolver->resolve($tokens, $template, $contract, Auth::user(), 'keep');
|
||||
foreach ($resolved['values'] as $k => $v) {
|
||||
$values['{'.$k.'}'] = $v;
|
||||
}
|
||||
}
|
||||
$note = strtr($rawNote, $values);
|
||||
}
|
||||
|
||||
Activity::create(array_filter([
|
||||
'note' => $note,
|
||||
'action_id' => $template->action_id,
|
||||
'decision_id' => $template->decision_id,
|
||||
'contract_id' => $contract->id,
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
], fn ($v) => ! is_null($v) && $v !== ''));
|
||||
} catch (\Throwable $e) {
|
||||
// swallow activity creation errors to not block document generation
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'document_uuid' => $doc->uuid,
|
||||
'path' => $doc->path,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Document;
|
||||
use App\Models\FieldJob;
|
||||
use App\Models\Import; // assuming model name Import
|
||||
use App\Models\Activity; // if this model exists
|
||||
use App\Models\Contract;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\CarbonPeriod;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
$today = now()->startOfDay();
|
||||
$yesterday = now()->subDay()->startOfDay();
|
||||
$staleThreshold = now()->subDays(7); // assumption: stale if no activity in last 7 days
|
||||
|
||||
$clientsTotal = Client::count();
|
||||
$clientsNew7d = Client::where('created_at', '>=', now()->subDays(7))->count();
|
||||
// FieldJob table does not have a scheduled_at column (schema shows: assigned_at, completed_at, cancelled_at)
|
||||
// Temporary logic: if scheduled_at ever added we'll use it; otherwise fall back to assigned_at then created_at.
|
||||
if (Schema::hasColumn('field_jobs', 'scheduled_at')) {
|
||||
$fieldJobsToday = FieldJob::whereDate('scheduled_at', $today)->count();
|
||||
} else {
|
||||
// Prefer assigned_at when present, otherwise created_at
|
||||
$fieldJobsToday = FieldJob::whereDate(DB::raw('COALESCE(assigned_at, created_at)'), $today)->count();
|
||||
}
|
||||
$documentsToday = Document::whereDate('created_at', $today)->count();
|
||||
$activeImports = Import::whereIn('status', ['queued', 'processing'])->count();
|
||||
$activeContracts = Contract::where('active', 1)->count();
|
||||
|
||||
// Basic activities deferred list (limit 10)
|
||||
$activities = Activity::query()
|
||||
->with(['clientCase:id,uuid'])
|
||||
->latest()
|
||||
->limit(10)
|
||||
->get(['id','note','created_at','client_case_id','contract_id','action_id','decision_id'])
|
||||
->map(fn($a) => [
|
||||
'id' => $a->id,
|
||||
'note' => $a->note,
|
||||
'created_at' => $a->created_at,
|
||||
'client_case_id' => $a->client_case_id,
|
||||
'client_case_uuid' => $a->clientCase?->uuid,
|
||||
'contract_id' => $a->contract_id,
|
||||
'action_id' => $a->action_id,
|
||||
'decision_id' => $a->decision_id,
|
||||
]);
|
||||
|
||||
// 7-day trends (including today)
|
||||
$start = now()->subDays(6)->startOfDay();
|
||||
$end = now()->endOfDay();
|
||||
|
||||
$dateKeys = collect(range(0,6))
|
||||
->map(fn($i) => now()->subDays(6 - $i)->format('Y-m-d'));
|
||||
|
||||
$clientTrendRaw = Client::whereBetween('created_at', [$start,$end])
|
||||
->selectRaw('DATE(created_at) as d, COUNT(*) as c')
|
||||
->groupBy('d')
|
||||
->pluck('c','d');
|
||||
$documentTrendRaw = Document::whereBetween('created_at', [$start,$end])
|
||||
->selectRaw('DATE(created_at) as d, COUNT(*) as c')
|
||||
->groupBy('d')
|
||||
->pluck('c','d');
|
||||
$fieldJobTrendRaw = FieldJob::whereBetween(DB::raw('COALESCE(assigned_at, created_at)'), [$start,$end])
|
||||
->selectRaw('DATE(COALESCE(assigned_at, created_at)) as d, COUNT(*) as c')
|
||||
->groupBy('d')
|
||||
->pluck('c','d');
|
||||
$importTrendRaw = Import::whereBetween('created_at', [$start,$end])
|
||||
->selectRaw('DATE(created_at) as d, COUNT(*) as c')
|
||||
->groupBy('d')
|
||||
->pluck('c','d');
|
||||
|
||||
// Completed field jobs last 7 days
|
||||
$fieldJobCompletedRaw = FieldJob::whereNotNull('completed_at')
|
||||
->whereBetween('completed_at', [$start, $end])
|
||||
->selectRaw('DATE(completed_at) as d, COUNT(*) as c')
|
||||
->groupBy('d')
|
||||
->pluck('c','d');
|
||||
|
||||
$trends = [
|
||||
'clients_new' => $dateKeys->map(fn($d) => (int) ($clientTrendRaw[$d] ?? 0))->values(),
|
||||
'documents_new' => $dateKeys->map(fn($d) => (int) ($documentTrendRaw[$d] ?? 0))->values(),
|
||||
'field_jobs' => $dateKeys->map(fn($d) => (int) ($fieldJobTrendRaw[$d] ?? 0))->values(),
|
||||
'imports_new' => $dateKeys->map(fn($d) => (int) ($importTrendRaw[$d] ?? 0))->values(),
|
||||
'field_jobs_completed' => $dateKeys->map(fn($d) => (int) ($fieldJobCompletedRaw[$d] ?? 0))->values(),
|
||||
'labels' => $dateKeys,
|
||||
];
|
||||
|
||||
// Stale client cases (no activity in last 7 days)
|
||||
$staleCases = \App\Models\ClientCase::query()
|
||||
->leftJoin('activities', function($join) {
|
||||
$join->on('activities.client_case_id', '=', 'client_cases.id')
|
||||
->whereNull('activities.deleted_at');
|
||||
})
|
||||
->selectRaw('client_cases.id, client_cases.uuid, client_cases.client_ref, MAX(activities.created_at) as last_activity_at, client_cases.created_at')
|
||||
->groupBy('client_cases.id','client_cases.uuid','client_cases.client_ref','client_cases.created_at')
|
||||
->havingRaw('(MAX(activities.created_at) IS NULL OR MAX(activities.created_at) < ?) AND client_cases.created_at < ?', [$staleThreshold, $staleThreshold])
|
||||
->orderByRaw('last_activity_at NULLS FIRST, client_cases.created_at ASC')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn($c) => [
|
||||
'id' => $c->id,
|
||||
'uuid' => $c->uuid,
|
||||
'client_ref' => $c->client_ref,
|
||||
'last_activity_at' => $c->last_activity_at,
|
||||
'created_at' => $c->created_at,
|
||||
'days_stale' => $c->last_activity_at ? now()->diffInDays($c->last_activity_at) : now()->diffInDays($c->created_at),
|
||||
]);
|
||||
|
||||
// Field jobs assigned today
|
||||
$fieldJobsAssignedToday = FieldJob::query()
|
||||
->whereDate(DB::raw('COALESCE(assigned_at, created_at)'), $today)
|
||||
->select(['id','assigned_user_id','priority','assigned_at','created_at','contract_id'])
|
||||
->latest(DB::raw('COALESCE(assigned_at, created_at)'))
|
||||
->limit(15)
|
||||
->get();
|
||||
|
||||
// Imports in progress (queued / processing)
|
||||
$importsInProgress = Import::query()
|
||||
->whereIn('status', ['queued','processing'])
|
||||
->latest('created_at')
|
||||
->limit(10)
|
||||
->get(['id','uuid','file_name','status','total_rows','imported_rows','valid_rows','invalid_rows','started_at'])
|
||||
->map(fn($i) => [
|
||||
'id' => $i->id,
|
||||
'uuid' => $i->uuid,
|
||||
'file_name' => $i->file_name,
|
||||
'status' => $i->status,
|
||||
'total_rows' => $i->total_rows,
|
||||
'imported_rows' => $i->imported_rows,
|
||||
'valid_rows' => $i->valid_rows,
|
||||
'invalid_rows' => $i->invalid_rows,
|
||||
'progress_pct' => $i->total_rows ? round(($i->imported_rows / max(1,$i->total_rows))*100,1) : null,
|
||||
'started_at' => $i->started_at,
|
||||
]);
|
||||
|
||||
// Active document templates summary (active versions)
|
||||
$activeTemplates = \App\Models\DocumentTemplate::query()
|
||||
->where('active', true)
|
||||
->latest('updated_at')
|
||||
->limit(10)
|
||||
->get(['id','name','slug','version','updated_at']);
|
||||
|
||||
// System health (deferred)
|
||||
$queueBacklog = Schema::hasTable('jobs') ? DB::table('jobs')->count() : null;
|
||||
$failedJobs = Schema::hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : null;
|
||||
$recentActivity = Activity::query()->latest('created_at')->value('created_at');
|
||||
$lastActivityMinutes = null;
|
||||
if ($recentActivity) {
|
||||
// diffInMinutes is absolute (non-negative) but guard anyway & cast to int
|
||||
$lastActivityMinutes = (int) max(0, now()->diffInMinutes($recentActivity));
|
||||
}
|
||||
$systemHealth = [
|
||||
'queue_backlog' => $queueBacklog,
|
||||
'failed_jobs' => $failedJobs,
|
||||
'last_activity_minutes' => $lastActivityMinutes,
|
||||
'last_activity_iso' => $recentActivity?->toIso8601String(),
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'kpis' => [
|
||||
'clients_total' => $clientsTotal,
|
||||
'clients_new_7d' => $clientsNew7d,
|
||||
'field_jobs_today' => $fieldJobsToday,
|
||||
'documents_today' => $documentsToday,
|
||||
'active_imports' => $activeImports,
|
||||
'active_contracts' => $activeContracts,
|
||||
],
|
||||
'trends' => $trends,
|
||||
])->with([ // deferred props (Inertia v2 style)
|
||||
'activities' => fn () => $activities,
|
||||
'systemHealth' => fn () => $systemHealth,
|
||||
'staleCases' => fn () => $staleCases,
|
||||
'fieldJobsAssignedToday' => fn () => $fieldJobsAssignedToday,
|
||||
'importsInProgress' => fn () => $importsInProgress,
|
||||
'activeTemplates' => fn () => $activeTemplates,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsurePermission
|
||||
{
|
||||
public function handle(Request $request, Closure $next, ...$permissions): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
if ($user->hasRole('admin')) {
|
||||
return $next($request);
|
||||
}
|
||||
if (! $user->hasPermission($permissions)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureRole
|
||||
{
|
||||
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user || ! $user->hasRole($roles)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,22 @@ public function version(Request $request): ?string
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return array_merge(parent::share($request), [
|
||||
'auth' => [
|
||||
'user' => function () use ($request) {
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'roles' => $user->roles()->select('id', 'name', 'slug')->get(),
|
||||
'permissions' => $user->permissions()->pluck('slug')->values(),
|
||||
];
|
||||
},
|
||||
],
|
||||
'flash' => [
|
||||
'success' => fn () => $request->session()->get('success'),
|
||||
'error' => fn () => $request->session()->get('error'),
|
||||
@@ -65,14 +81,11 @@ public function share(Request $request): array
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
|
||||
|
||||
|
||||
return [
|
||||
'dueToday' => [
|
||||
'count' => $activities->count(),
|
||||
'items' => $activities,
|
||||
'date' => $today,
|
||||
'date' => $today,
|
||||
],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreDocumentTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
return $user && ($user->hasPermission('manage-document-templates') || $user->hasPermission('manage-settings') || $user->hasRole('admin'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
// Slug uniqueness enforced only for first version; controller will increment version if slug exists
|
||||
'slug' => ['required', 'string', 'max:255'],
|
||||
'custom_name' => ['nullable', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'file' => ['required', 'file', 'mimetypes:application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'max:4096'],
|
||||
'meta' => ['sometimes', 'array'],
|
||||
'meta.*' => ['nullable'],
|
||||
'action_id' => ['nullable', 'integer', 'exists:actions,id'], // New optional field
|
||||
'decision_id' => ['nullable', 'integer', 'exists:decisions,id'], // New optional field
|
||||
'activity_note_template' => ['nullable', 'string'], // New optional field
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'file.mimetypes' => 'Datoteka mora biti DOCX.',
|
||||
'file.max' => 'Datoteka je prevelika (max 4MB).',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePermissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasPermission('manage-settings') || $this->user()?->hasRole('admin');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:permissions,slug'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Ime je obvezno.',
|
||||
'slug.required' => 'Slug je obvezen.',
|
||||
'slug.unique' => 'Slug že obstaja.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDocumentTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$u = $this->user();
|
||||
|
||||
return $u && ($u->hasPermission('manage-document-templates') || $u->hasPermission('manage-settings') || $u->hasRole('admin'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'output_filename_pattern' => ['nullable', 'string', 'max:255'],
|
||||
'date_format' => ['nullable', 'string', 'max:40'],
|
||||
'fail_on_unresolved' => ['sometimes', 'boolean'],
|
||||
'number_decimals' => ['nullable', 'integer', 'min:0', 'max:6'],
|
||||
'decimal_separator' => ['nullable', 'string', 'max:2'],
|
||||
'thousands_separator' => ['nullable', 'string', 'max:2'],
|
||||
'currency_symbol' => ['nullable', 'string', 'max:8'],
|
||||
'currency_position' => ['nullable', 'in:before,after'],
|
||||
'currency_space' => ['nullable', 'boolean'],
|
||||
'default_date_format' => ['nullable', 'string', 'max:40'],
|
||||
'date_formats' => ['nullable', 'array'],
|
||||
'date_formats.*' => ['nullable', 'string', 'max:40'],
|
||||
'meta' => ['sometimes', 'array'],
|
||||
'meta.*' => ['nullable'],
|
||||
'action_id' => ['nullable', 'integer', 'exists:actions,id'],
|
||||
'decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||
'activity_note_template' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user