activity is now added when contract balance is changed

This commit is contained in:
Simon Pocrnjič 2026-04-02 21:44:15 +02:00
parent d54fc9914d
commit 342d9d0700
8 changed files with 326 additions and 1 deletions

View File

@ -223,7 +223,11 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
return back()->with('warning', __('contracts.edit_not_allowed_archived')); return back()->with('warning', __('contracts.edit_not_allowed_archived'));
} }
\DB::transaction(function () use ($request, $contract) { $balanceChanged = false;
$oldBalance = null;
$newBalance = null;
\DB::transaction(function () use ($request, $contract, &$balanceChanged, &$oldBalance, &$newBalance) {
$contract->update([ $contract->update([
'reference' => $request->input('reference'), 'reference' => $request->input('reference'),
'type_id' => $request->input('type_id'), 'type_id' => $request->input('type_id'),
@ -254,6 +258,7 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
$accountData['type_id'] = $request->input('account_type_id'); $accountData['type_id'] = $request->input('account_type_id');
} }
if ($currentAccount) { if ($currentAccount) {
$oldBalance = (float) $currentAccount->balance_amount;
$currentAccount->update($accountData); $currentAccount->update($accountData);
if (array_key_exists('balance_amount', $accountData)) { if (array_key_exists('balance_amount', $accountData)) {
$currentAccount->forceFill(['balance_amount' => $accountData['balance_amount']])->save(); $currentAccount->forceFill(['balance_amount' => $accountData['balance_amount']])->save();
@ -264,6 +269,10 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
->update(['balance_amount' => $accountData['balance_amount'], 'updated_at' => now()]); ->update(['balance_amount' => $accountData['balance_amount'], 'updated_at' => now()]);
$freshBal = (float) optional($currentAccount->fresh())->balance_amount; $freshBal = (float) optional($currentAccount->fresh())->balance_amount;
} }
$newBalance = $freshBal;
if ($oldBalance !== $freshBal) {
$balanceChanged = true;
}
} else { } else {
$freshBal = (float) optional($currentAccount->fresh())->balance_amount; $freshBal = (float) optional($currentAccount->fresh())->balance_amount;
} }
@ -276,6 +285,27 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
}); });
// Fire activity if balance changed and settings require it
if ($balanceChanged) {
$contractSetting = \App\Models\ContractSetting::query()->first();
if ($contractSetting && $contractSetting->create_activity_on_balance_change) {
$note = str_replace(
['{old_balance}', '{new_balance}', '{currency}'],
[number_format($oldBalance, 2, '.', ''), number_format($newBalance, 2, '.', ''), 'EUR'],
$contractSetting->activity_note_template ?? ''
);
\App\Models\Activity::query()->create([
'due_date' => null,
'amount' => $newBalance,
'note' => $note,
'action_id' => $contractSetting->default_action_id,
'decision_id' => $contractSetting->default_decision_id,
'client_case_id' => $contract->client_case_id,
'contract_id' => $contract->id,
]);
}
}
// Preserve segment filter if present // Preserve segment filter if present
$segment = request('segment'); $segment = request('segment');

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
class ContractSettingController extends Controller
{
public function edit(): \Inertia\Response
{
$setting = \App\Models\ContractSetting::query()->first();
if (! $setting) {
$setting = \App\Models\ContractSetting::query()->create([
'create_activity_on_balance_change' => false,
'default_action_id' => null,
'default_decision_id' => null,
'activity_note_template' => 'Sprememba stanja pogodbe: {old_balance} → {new_balance} {currency}',
]);
}
$decisions = \App\Models\Decision::query()->orderBy('name')->get(['id', 'name']);
$actions = \App\Models\Action::query()
->with(['decisions:id'])
->orderBy('name')
->get()
->map(function (\App\Models\Action $a) {
return [
'id' => $a->id,
'name' => $a->name,
'decision_ids' => $a->decisions->pluck('id')->values(),
];
});
return \Inertia\Inertia::render('Settings/Contracts/Index', [
'setting' => [
'id' => $setting->id,
'create_activity_on_balance_change' => (bool) $setting->create_activity_on_balance_change,
'default_action_id' => $setting->default_action_id,
'default_decision_id' => $setting->default_decision_id,
'activity_note_template' => $setting->activity_note_template,
],
'decisions' => $decisions,
'actions' => $actions,
]);
}
public function update(\App\Http\Requests\UpdateContractSettingRequest $request): \Illuminate\Http\RedirectResponse
{
$data = $request->validated();
$setting = \App\Models\ContractSetting::query()->firstOrFail();
$data['create_activity_on_balance_change'] = (bool) ($data['create_activity_on_balance_change'] ?? false);
$setting->update($data);
return back()->with('success', 'Nastavitve shranjene.');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateContractSettingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'create_activity_on_balance_change' => ['sometimes', 'boolean'],
'default_action_id' => ['nullable', 'integer', 'exists:actions,id'],
'default_decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
'activity_note_template' => ['nullable', 'string', 'max:255'],
];
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContractSetting extends Model
{
protected $fillable = [
'create_activity_on_balance_change',
'default_action_id',
'default_decision_id',
'activity_note_template',
];
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contract_settings', function (Blueprint $table): void {
$table->id();
$table->boolean('create_activity_on_balance_change')->default(false);
$table->foreignId('default_action_id')->nullable()->constrained('actions')->nullOnDelete();
$table->foreignId('default_decision_id')->nullable()->constrained('decisions')->nullOnDelete();
$table->string('activity_note_template', 255)->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contract_settings');
}
};

View File

@ -0,0 +1,159 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm } from "@inertiajs/vue3";
import { computed, watch } from "vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { FileText } from "lucide-vue-next";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import InputLabel from "@/Components/InputLabel.vue";
import { Checkbox } from "@/Components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
setting: Object,
decisions: Array,
actions: Array,
});
const form = useForm({
create_activity_on_balance_change: !!props.setting?.create_activity_on_balance_change,
default_action_id: props.setting?.default_action_id ?? null,
default_decision_id: props.setting?.default_decision_id ?? null,
activity_note_template:
props.setting?.activity_note_template ??
"Sprememba stanja pogodbe: {old_balance} → {new_balance} {currency}",
});
const filteredDecisions = computed(() => {
const actionId = form.default_action_id;
if (!actionId) return [];
const action = props.actions?.find((a) => a.id === actionId);
if (!action || !action.decision_ids) return [];
const ids = new Set(action.decision_ids);
return (props.decisions || []).filter((d) => ids.has(d.id));
});
watch(
() => form.default_action_id,
(newVal) => {
if (!newVal) {
form.default_decision_id = null;
} else {
const ids = new Set((filteredDecisions.value || []).map((d) => d.id));
if (!ids.has(form.default_decision_id)) {
form.default_decision_id = null;
}
}
}
);
const submit = () => {
form.put(route("settings.contract.update"), {
preserveScroll: true,
});
};
</script>
<template>
<AppLayout title="Nastavitve pogodb">
<template #header></template>
<div class="max-w-3xl mx-auto p-6">
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<FileText :size="18" />
<CardTitle class="uppercase">Nastavitve pogodb</CardTitle>
</div>
</template>
<div class="space-y-6 p-4 border-t">
<div class="flex items-center gap-2">
<Checkbox
id="create-activity"
v-model="form.create_activity_on_balance_change"
/>
<InputLabel for="create-activity" class="text-sm font-normal cursor-pointer">
Ustvari aktivnost ob spremembi odprtega zneska pogodbe
</InputLabel>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<InputLabel for="default-action">Privzeto dejanje</InputLabel>
<Select v-model="form.default_action_id">
<SelectTrigger id="default-action">
<SelectValue placeholder="— Brez —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Brez </SelectItem>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">{{
a.name
}}</SelectItem>
</SelectContent>
</Select>
<div v-if="form.errors.default_action_id" class="text-sm text-red-600 mt-1">
{{ form.errors.default_action_id }}
</div>
</div>
<div>
<InputLabel for="default-decision">Privzeta odločitev</InputLabel>
<Select
v-model="form.default_decision_id"
:disabled="!form.default_action_id"
>
<SelectTrigger id="default-decision">
<SelectValue placeholder="— Najprej izberite dejanje —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Najprej izberite dejanje </SelectItem>
<SelectItem v-for="d in filteredDecisions" :key="d.id" :value="d.id">{{
d.name
}}</SelectItem>
</SelectContent>
</Select>
<div
v-if="form.errors.default_decision_id"
class="text-sm text-red-600 mt-1"
>
{{ form.errors.default_decision_id }}
</div>
</div>
</div>
<div>
<InputLabel for="note-template">Predloga opombe aktivnosti</InputLabel>
<Input id="note-template" v-model="form.activity_note_template" />
<p class="text-xs text-gray-500 mt-1">
Podprti žetoni: {old_balance}, {new_balance}, {currency}
</p>
<div
v-if="form.errors.activity_note_template"
class="text-sm text-red-600 mt-1"
>
{{ form.errors.activity_note_template }}
</div>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="form.reset()">Ponastavi</Button>
<Button @click="submit" :disabled="form.processing">Shrani</Button>
</div>
</div>
</AppCard>
</div>
</AppLayout>
</template>

View File

@ -52,6 +52,12 @@ const settingsCards = [
route: "settings.contractConfigs.index", route: "settings.contractConfigs.index",
icon: FileText, icon: FileText,
}, },
{
title: "Contract Settings",
description: "Auto-activity triggers on contract balance changes.",
route: "settings.contract.edit",
icon: FileText,
},
{ {
title: "Archive Settings", title: "Archive Settings",
description: "Define rules for archiving or soft-deleting aged data.", description: "Define rules for archiving or soft-deleting aged data.",

View File

@ -9,6 +9,7 @@
use App\Http\Controllers\ClientCaseContoller; use App\Http\Controllers\ClientCaseContoller;
use App\Http\Controllers\ClientController; use App\Http\Controllers\ClientController;
use App\Http\Controllers\ContractConfigController; use App\Http\Controllers\ContractConfigController;
use App\Http\Controllers\ContractSettingController;
use App\Http\Controllers\FieldJobController; use App\Http\Controllers\FieldJobController;
use App\Http\Controllers\FieldJobSettingController; use App\Http\Controllers\FieldJobSettingController;
use App\Http\Controllers\ImportController; use App\Http\Controllers\ImportController;
@ -518,6 +519,10 @@
Route::get('settings/installment', [InstallmentSettingController::class, 'edit'])->name('settings.installment.edit'); Route::get('settings/installment', [InstallmentSettingController::class, 'edit'])->name('settings.installment.edit');
Route::put('settings/installment', [InstallmentSettingController::class, 'update'])->name('settings.installment.update'); Route::put('settings/installment', [InstallmentSettingController::class, 'update'])->name('settings.installment.update');
// settings - contract settings
Route::get('settings/contract', [ContractSettingController::class, 'edit'])->name('settings.contract.edit');
Route::put('settings/contract', [ContractSettingController::class, 'update'])->name('settings.contract.update');
Route::get('types/address', function (Request $request) { Route::get('types/address', function (Request $request) {
$types = App\Models\Person\AddressType::all(); $types = App\Models\Person\AddressType::all();