add payment option
This commit is contained in:
parent
0e0912c81b
commit
971a9e89d1
53
app/Http/Controllers/AccountBookingController.php
Normal file
53
app/Http/Controllers/AccountBookingController.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreBookingRequest;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Booking;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class AccountBookingController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Account $account): Response
|
||||||
|
{
|
||||||
|
$bookings = Booking::query()
|
||||||
|
->where('account_id', $account->id)
|
||||||
|
->orderByDesc('booked_at')
|
||||||
|
->get(['id', 'payment_id', 'amount_cents', 'type', 'description', 'booked_at', 'created_at']);
|
||||||
|
|
||||||
|
return Inertia::render('Accounts/Bookings/Index', [
|
||||||
|
'account' => $account->only(['id', 'reference', 'description']),
|
||||||
|
'bookings' => $bookings,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreBookingRequest $request, Account $account): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
Booking::query()->create([
|
||||||
|
'account_id' => $account->id,
|
||||||
|
'payment_id' => $validated['payment_id'] ?? null,
|
||||||
|
'amount_cents' => (int) round(((float) $validated['amount']) * 100),
|
||||||
|
'type' => $validated['type'],
|
||||||
|
'description' => $validated['description'] ?? null,
|
||||||
|
'booked_at' => $validated['booked_at'] ?? now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Booking created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Account $account, Booking $booking): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($booking->account_id !== $account->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Booking deleted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
150
app/Http/Controllers/AccountPaymentController.php
Normal file
150
app/Http/Controllers/AccountPaymentController.php
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StorePaymentRequest;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Activity;
|
||||||
|
use App\Models\Booking;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\PaymentSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class AccountPaymentController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Account $account): Response
|
||||||
|
{
|
||||||
|
$payments = Payment::query()
|
||||||
|
->where('account_id', $account->id)
|
||||||
|
->orderByDesc('paid_at')
|
||||||
|
->get(['id', 'amount_cents', 'currency', 'reference', 'paid_at', 'created_at'])
|
||||||
|
->map(function (Payment $p) {
|
||||||
|
return [
|
||||||
|
'id' => $p->id,
|
||||||
|
'amount' => $p->amount, // accessor divides cents
|
||||||
|
'currency' => $p->currency,
|
||||||
|
'reference' => $p->reference,
|
||||||
|
'paid_at' => $p->paid_at,
|
||||||
|
'created_at' => $p->created_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('Accounts/Payments/Index', [
|
||||||
|
'account' => $account->only(['id', 'reference', 'description']),
|
||||||
|
'payments' => $payments,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(Account $account): JsonResponse
|
||||||
|
{
|
||||||
|
$payments = Payment::query()
|
||||||
|
->where('account_id', $account->id)
|
||||||
|
->orderByDesc('paid_at')
|
||||||
|
->get(['id', 'amount_cents', 'currency', 'reference', 'paid_at', 'created_at'])
|
||||||
|
->map(function (Payment $p) {
|
||||||
|
return [
|
||||||
|
'id' => $p->id,
|
||||||
|
'amount' => $p->amount,
|
||||||
|
'currency' => $p->currency,
|
||||||
|
'reference' => $p->reference,
|
||||||
|
'paid_at' => optional($p->paid_at)?->toDateString(),
|
||||||
|
'created_at' => optional($p->created_at)?->toDateTimeString(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'account' => [
|
||||||
|
'id' => $account->id,
|
||||||
|
'balance_amount' => $account->balance_amount,
|
||||||
|
],
|
||||||
|
'payments' => $payments,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StorePaymentRequest $request, Account $account): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$amountCents = (int) round(((float) $validated['amount']) * 100);
|
||||||
|
|
||||||
|
// Load defaults from settings
|
||||||
|
$settings = PaymentSetting::query()->first();
|
||||||
|
$defaultCurrency = strtoupper($settings->default_currency ?? 'EUR');
|
||||||
|
|
||||||
|
$payment = Payment::query()->create([
|
||||||
|
'account_id' => $account->id,
|
||||||
|
'amount_cents' => $amountCents,
|
||||||
|
'currency' => strtoupper($validated['currency'] ?? $defaultCurrency),
|
||||||
|
'reference' => $validated['reference'] ?? null,
|
||||||
|
'paid_at' => $validated['paid_at'] ?? now(),
|
||||||
|
'meta' => $validated['meta'] ?? null,
|
||||||
|
'created_by' => $request->user()?->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-create a credit booking for this payment to reduce account balance
|
||||||
|
Booking::query()->create([
|
||||||
|
'account_id' => $account->id,
|
||||||
|
'payment_id' => $payment->id,
|
||||||
|
'amount_cents' => $amountCents,
|
||||||
|
'type' => 'credit',
|
||||||
|
'description' => $payment->reference ? ('Plačilo '.$payment->reference) : 'Plačilo',
|
||||||
|
'booked_at' => $payment->paid_at ?? now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optionally create an activity entry with default decision/action
|
||||||
|
if ($settings && ($settings->create_activity_on_payment ?? false)) {
|
||||||
|
$note = $settings->activity_note_template ?? 'Prejeto plačilo';
|
||||||
|
$note = str_replace(['{amount}', '{currency}'], [number_format($amountCents / 100, 2, ',', '.'), $payment->currency], $note);
|
||||||
|
$account->loadMissing('contract');
|
||||||
|
$clientCaseId = $account->contract?->client_case_id;
|
||||||
|
if ($clientCaseId) {
|
||||||
|
$activity = Activity::query()->create([
|
||||||
|
'due_date' => null,
|
||||||
|
'amount' => $amountCents / 100,
|
||||||
|
'note' => $note,
|
||||||
|
'action_id' => $settings->default_action_id,
|
||||||
|
'decision_id' => $settings->default_decision_id,
|
||||||
|
'client_case_id' => $clientCaseId,
|
||||||
|
'contract_id' => $account->contract_id,
|
||||||
|
]);
|
||||||
|
// Link the payment to the activity
|
||||||
|
$payment->update(['activity_id' => $activity->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Payment created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Account $account, Payment $payment): RedirectResponse|JsonResponse
|
||||||
|
{
|
||||||
|
if ($payment->account_id !== $account->id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete related booking(s) to revert balance via model events
|
||||||
|
Booking::query()->where('payment_id', $payment->id)->get()->each->delete();
|
||||||
|
|
||||||
|
// Optionally delete related activity
|
||||||
|
if ($payment->activity_id ?? null) {
|
||||||
|
$activity = Activity::query()->find($payment->activity_id);
|
||||||
|
if ($activity) {
|
||||||
|
$activity->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$payment->delete();
|
||||||
|
|
||||||
|
if (request()->wantsJson()) {
|
||||||
|
$account->refresh();
|
||||||
|
return response()->json([
|
||||||
|
'ok' => true,
|
||||||
|
'balance_amount' => $account->balance_amount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Payment deleted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Http/Controllers/PaymentSettingController.php
Normal file
67
app/Http/Controllers/PaymentSettingController.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\UpdatePaymentSettingRequest;
|
||||||
|
use App\Models\Action;
|
||||||
|
use App\Models\Decision;
|
||||||
|
use App\Models\PaymentSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class PaymentSettingController extends Controller
|
||||||
|
{
|
||||||
|
public function edit(): Response
|
||||||
|
{
|
||||||
|
$setting = PaymentSetting::query()->first();
|
||||||
|
if (! $setting) {
|
||||||
|
$setting = PaymentSetting::query()->create([
|
||||||
|
'default_currency' => 'EUR',
|
||||||
|
'create_activity_on_payment' => false,
|
||||||
|
'default_decision_id' => null,
|
||||||
|
'default_action_id' => null,
|
||||||
|
'activity_note_template' => 'Prejeto plačilo: {amount} {currency}',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisions = Decision::query()->orderBy('name')->get(['id', 'name']);
|
||||||
|
$actions = Action::query()
|
||||||
|
->with(['decisions:id'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(function (Action $a) {
|
||||||
|
return [
|
||||||
|
'id' => $a->id,
|
||||||
|
'name' => $a->name,
|
||||||
|
'decision_ids' => $a->decisions->pluck('id')->values(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('Settings/Payments/Index', [
|
||||||
|
'setting' => [
|
||||||
|
'id' => $setting->id,
|
||||||
|
'default_currency' => $setting->default_currency,
|
||||||
|
'create_activity_on_payment' => (bool) $setting->create_activity_on_payment,
|
||||||
|
'default_decision_id' => $setting->default_decision_id,
|
||||||
|
'default_action_id' => $setting->default_action_id,
|
||||||
|
'activity_note_template' => $setting->activity_note_template,
|
||||||
|
],
|
||||||
|
'decisions' => $decisions,
|
||||||
|
'actions' => $actions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdatePaymentSettingRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
$setting = PaymentSetting::query()->firstOrFail();
|
||||||
|
|
||||||
|
// Ensure boolean cast for checkbox
|
||||||
|
$data['create_activity_on_payment'] = (bool) ($data['create_activity_on_payment'] ?? false);
|
||||||
|
|
||||||
|
$setting->fill($data)->save();
|
||||||
|
|
||||||
|
return back()->with('success', 'Payment settings updated.');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Requests/StoreBookingRequest.php
Normal file
24
app/Http/Requests/StoreBookingRequest.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreBookingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||||
|
'type' => ['required', 'in:debit,credit'],
|
||||||
|
'description' => ['nullable', 'string', 'max:255'],
|
||||||
|
'booked_at' => ['nullable', 'date'],
|
||||||
|
'payment_id' => ['nullable', 'integer', 'exists:payments,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Requests/StorePaymentRequest.php
Normal file
24
app/Http/Requests/StorePaymentRequest.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StorePaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||||
|
'currency' => ['nullable', 'string', 'size:3'],
|
||||||
|
'reference' => ['nullable', 'string', 'max:100'],
|
||||||
|
'paid_at' => ['nullable', 'date'],
|
||||||
|
'meta' => ['nullable', 'array'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Requests/UpdatePaymentSettingRequest.php
Normal file
24
app/Http/Requests/UpdatePaymentSettingRequest.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdatePaymentSettingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'default_currency' => ['required', 'string', 'size:3'],
|
||||||
|
'create_activity_on_payment' => ['sometimes', 'boolean'],
|
||||||
|
'default_decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||||
|
'default_action_id' => ['nullable', 'integer', 'exists:actions,id'],
|
||||||
|
'activity_note_template' => ['nullable', 'string', 'max:255'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,4 +42,18 @@ public function debts(): HasMany
|
||||||
return $this->hasMany(\App\Models\Debt::class);
|
return $this->hasMany(\App\Models\Debt::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\Payment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bookings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\Booking::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contract(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\Contract::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,9 @@
|
||||||
class AccountType extends Model
|
class AccountType extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
app/Models/Booking.php
Normal file
81
app/Models/Booking.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Booking extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'account_id',
|
||||||
|
'payment_id',
|
||||||
|
'amount_cents',
|
||||||
|
'type',
|
||||||
|
'description',
|
||||||
|
'booked_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'booked_at' => 'datetime',
|
||||||
|
'amount_cents' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function account(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Account::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payment(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Payment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::created(function (Booking $booking): void {
|
||||||
|
$booking->applyToAccountBalance(+1);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleted(function (Booking $booking): void {
|
||||||
|
// Soft delete should revert the effect on balance
|
||||||
|
$booking->applyToAccountBalance(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::restored(function (Booking $booking): void {
|
||||||
|
// Re-apply when restored
|
||||||
|
$booking->applyToAccountBalance(+1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply or revert the booking effect on account balance.
|
||||||
|
*
|
||||||
|
* @param int $multiplier +1 to apply, -1 to revert
|
||||||
|
*/
|
||||||
|
protected function applyToAccountBalance(int $multiplier = 1): void
|
||||||
|
{
|
||||||
|
$account = $this->account;
|
||||||
|
if (! $account) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$delta = ($this->amount_cents / 100.0);
|
||||||
|
if ($this->type === 'credit') {
|
||||||
|
// Credit decreases the receivable (balance goes down)
|
||||||
|
$delta = -$delta;
|
||||||
|
}
|
||||||
|
// Debit increases receivable (balance up), credit decreases
|
||||||
|
$account->forceFill([
|
||||||
|
'balance_amount' => (float) ($account->balance_amount ?? 0) + ($multiplier * $delta),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,76 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use App\Models\Activity;
|
||||||
|
|
||||||
class Payment extends Model
|
class Payment extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'account_id',
|
||||||
|
'amount_cents',
|
||||||
|
'currency',
|
||||||
|
'reference',
|
||||||
|
'paid_at',
|
||||||
|
'meta',
|
||||||
|
'created_by',
|
||||||
|
'activity_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
'meta' => 'array',
|
||||||
|
'amount_cents' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function account(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Account::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bookings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Booking::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activity(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Activity::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function type(): BelongsTo
|
public function type(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\PaymentType::class);
|
return $this->belongsTo(\App\Models\PaymentType::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessor to expose decimal amount for JSON serialization and UI convenience.
|
||||||
|
*/
|
||||||
|
protected function amount(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::get(function () {
|
||||||
|
$cents = (int) ($this->attributes['amount_cents'] ?? 0);
|
||||||
|
|
||||||
|
return $cents / 100;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutator to set amount via decimal; stores in cents.
|
||||||
|
*/
|
||||||
|
public function setAmountAttribute($value): void
|
||||||
|
{
|
||||||
|
$this->attributes['amount_cents'] = (int) round(((float) $value) * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
app/Models/PaymentSetting.php
Normal file
19
app/Models/PaymentSetting.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentSetting extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'default_currency',
|
||||||
|
'create_activity_on_payment',
|
||||||
|
'default_decision_id',
|
||||||
|
'default_action_id',
|
||||||
|
'activity_note_template',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('payments')) {
|
||||||
|
Schema::create('payments', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('account_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->bigInteger('amount_cents');
|
||||||
|
$table->string('currency', 3)->default('EUR');
|
||||||
|
$table->string('reference')->nullable();
|
||||||
|
$table->timestamp('paid_at')->nullable();
|
||||||
|
$table->jsonb('meta')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->index(['account_id', 'paid_at']);
|
||||||
|
$table->index('reference');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('payments');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('bookings')) {
|
||||||
|
Schema::create('bookings', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('account_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('payment_id')->nullable()->constrained('payments')->nullOnDelete();
|
||||||
|
$table->bigInteger('amount_cents');
|
||||||
|
$table->enum('type', ['debit', 'credit']);
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->timestamp('booked_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->index(['account_id', 'booked_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bookings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Drop in reverse dependency order
|
||||||
|
if (Schema::hasTable('bookings')) {
|
||||||
|
Schema::drop('bookings');
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('payments')) {
|
||||||
|
Schema::drop('payments');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate payments
|
||||||
|
Schema::create('payments', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('account_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->bigInteger('amount_cents');
|
||||||
|
$table->string('currency', 3)->default('EUR');
|
||||||
|
$table->string('reference')->nullable();
|
||||||
|
$table->timestamp('paid_at')->nullable();
|
||||||
|
$table->jsonb('meta')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->index(['account_id', 'paid_at']);
|
||||||
|
$table->index('reference');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate bookings
|
||||||
|
Schema::create('bookings', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('account_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('payment_id')->nullable()->constrained('payments')->nullOnDelete();
|
||||||
|
$table->bigInteger('amount_cents');
|
||||||
|
$table->enum('type', ['debit', 'credit']);
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->timestamp('booked_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->index(['account_id', 'booked_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('bookings')) {
|
||||||
|
Schema::drop('bookings');
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('payments')) {
|
||||||
|
Schema::drop('payments');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('payment_settings', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('default_currency', 3)->default('EUR');
|
||||||
|
$table->boolean('create_activity_on_payment')->default(false);
|
||||||
|
$table->foreignId('default_decision_id')->nullable()->constrained('decisions')->nullOnDelete();
|
||||||
|
$table->foreignId('default_action_id')->nullable()->constrained('actions')->nullOnDelete();
|
||||||
|
$table->string('activity_note_template', 255)->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('payment_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('payments') && ! Schema::hasColumn('payments', 'activity_id')) {
|
||||||
|
Schema::table('payments', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('activity_id')->nullable()->constrained('activities')->nullOnDelete()->after('created_by');
|
||||||
|
$table->index('activity_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('payments') && Schema::hasColumn('payments', 'activity_id')) {
|
||||||
|
Schema::table('payments', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('activity_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
use App\Models\AccountType;
|
use App\Models\AccountType;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class AccountTypeSeeder extends Seeder
|
class AccountTypeSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|
@ -12,40 +11,16 @@ public function run(): void
|
||||||
{
|
{
|
||||||
$now = now();
|
$now = now();
|
||||||
|
|
||||||
// If table is empty, insert with explicit IDs so id=1 exists (matches default logic elsewhere)
|
$rows = [
|
||||||
if (AccountType::count() === 0) {
|
['name' => 'Receivables', 'description' => 'Standard receivable account'],
|
||||||
$rows = [
|
['name' => 'Payables', 'description' => 'Standard payable account'],
|
||||||
['id' => 1, 'name' => 'Default', 'description' => 'Default account type', 'created_at' => $now, 'updated_at' => $now],
|
['name' => 'Loan', 'description' => 'Loan and credit account'],
|
||||||
['id' => 2, 'name' => 'Primary', 'description' => 'Primary account', 'created_at' => $now, 'updated_at' => $now],
|
['name' => 'Savings', 'description' => 'Savings account type'],
|
||||||
['id' => 3, 'name' => 'Secondary', 'description' => 'Secondary account', 'created_at' => $now, 'updated_at' => $now],
|
['name' => 'Current', 'description' => 'Current/operational account'],
|
||||||
['id' => 4, 'name' => 'Savings', 'description' => 'Savings account', 'created_at' => $now, 'updated_at' => $now],
|
|
||||||
['id' => 5, 'name' => 'Checking', 'description' => 'Checking account', 'created_at' => $now, 'updated_at' => $now],
|
|
||||||
['id' => 6, 'name' => 'Credit', 'description' => 'Credit account', 'created_at' => $now, 'updated_at' => $now],
|
|
||||||
['id' => 7, 'name' => 'Loan', 'description' => 'Loan account', 'created_at' => $now, 'updated_at' => $now],
|
|
||||||
['id' => 8, 'name' => 'Other', 'description' => 'Other account type', 'created_at' => $now, 'updated_at' => $now],
|
|
||||||
];
|
|
||||||
DB::table('account_types')->insert($rows);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If table already has data, ensure the basics exist (idempotent, no explicit IDs)
|
|
||||||
$names = [
|
|
||||||
'Default' => 'Default account type',
|
|
||||||
'Primary' => 'Primary account',
|
|
||||||
'Secondary' => 'Secondary account',
|
|
||||||
'Savings' => 'Savings account',
|
|
||||||
'Checking' => 'Checking account',
|
|
||||||
'Credit' => 'Credit account',
|
|
||||||
'Loan' => 'Loan account',
|
|
||||||
'Other' => 'Other account type',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($names as $name => $desc) {
|
foreach ($rows as $row) {
|
||||||
AccountType::updateOrCreate(
|
AccountType::updateOrCreate(['name' => $row['name']], ['description' => $row['description']]);
|
||||||
['name' => $name],
|
|
||||||
['description' => $desc]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ public function run(): void
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->call([
|
$this->call([
|
||||||
|
AccountTypeSeeder::class,
|
||||||
|
PaymentSettingSeeder::class,
|
||||||
PersonSeeder::class,
|
PersonSeeder::class,
|
||||||
SegmentSeeder::class,
|
SegmentSeeder::class,
|
||||||
ActionSeeder::class,
|
ActionSeeder::class,
|
||||||
|
|
|
||||||
21
database/seeders/PaymentSettingSeeder.php
Normal file
21
database/seeders/PaymentSettingSeeder.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\PaymentSetting;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class PaymentSettingSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create default record if not exists
|
||||||
|
PaymentSetting::query()->firstOrCreate([], [
|
||||||
|
'default_currency' => 'EUR',
|
||||||
|
'create_activity_on_payment' => false,
|
||||||
|
'default_decision_id' => null,
|
||||||
|
'default_action_id' => null,
|
||||||
|
'activity_note_template' => 'Prejeto plačilo: {amount} {currency}',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
resources/js/Pages/Accounts/Bookings/Index.vue
Normal file
122
resources/js/Pages/Accounts/Bookings/Index.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script setup>
|
||||||
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
|
import { useForm, Link } from '@inertiajs/vue3'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
account: Object,
|
||||||
|
bookings: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const showCreate = ref(false)
|
||||||
|
const form = useForm({
|
||||||
|
amount: '',
|
||||||
|
type: 'credit',
|
||||||
|
description: '',
|
||||||
|
booked_at: '',
|
||||||
|
payment_id: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
form.reset()
|
||||||
|
form.clearErrors()
|
||||||
|
showCreate.value = true
|
||||||
|
}
|
||||||
|
function closeCreate() {
|
||||||
|
showCreate.value = false
|
||||||
|
form.clearErrors()
|
||||||
|
}
|
||||||
|
function submit() {
|
||||||
|
form.post(route('accounts.bookings.store', { account: props.account.id }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => closeCreate(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppLayout :title="`Bookings - ${account.reference || account.id}`">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Bookings · {{ account.reference || account.id }}</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Link :href="route('accounts.payments.index', { account: account.id })" class="text-sm underline">Payments</Link>
|
||||||
|
<button class="px-3 py-2 bg-emerald-600 text-white rounded" @click="openCreate">New Booking</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||||
|
<table class="min-w-full text-left text-sm">
|
||||||
|
<thead class="border-b text-gray-500">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 pr-4">Booked at</th>
|
||||||
|
<th class="py-2 pr-4">Type</th>
|
||||||
|
<th class="py-2 pr-4">Amount</th>
|
||||||
|
<th class="py-2 pr-4">Description</th>
|
||||||
|
<th class="py-2 pr-0 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="b in bookings" :key="b.id" class="border-b last:border-0">
|
||||||
|
<td class="py-2 pr-4">{{ b.booked_at ? new Date(b.booked_at).toLocaleString() : '-' }}</td>
|
||||||
|
<td class="py-2 pr-4">
|
||||||
|
<span :class="['px-2 py-0.5 rounded text-xs', b.type === 'credit' ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-800']">
|
||||||
|
{{ b.type }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 font-medium">{{ (b.amount_cents / 100).toFixed(2) }}</td>
|
||||||
|
<td class="py-2 pr-4">{{ b.description || '-' }}</td>
|
||||||
|
<td class="py-2 pr-0 text-right">
|
||||||
|
<form :action="route('accounts.bookings.destroy', { account: account.id, booking: b.id })" method="post" @submit.prevent="$inertia.delete(route('accounts.bookings.destroy', { account: account.id, booking: b.id }), { preserveScroll: true })">
|
||||||
|
<button type="submit" class="text-red-600 hover:underline">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!bookings?.length">
|
||||||
|
<td colspan="5" class="py-6 text-center text-gray-500">No bookings.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showCreate" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div class="absolute inset-0 bg-black/30" @click="closeCreate"></div>
|
||||||
|
<div class="relative bg-white rounded shadow-lg w-[32rem] max-w-[90%] p-5">
|
||||||
|
<div class="text-lg font-semibold mb-2">New Booking</div>
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Type</label>
|
||||||
|
<select v-model="form.type" class="w-full border rounded px-3 py-2">
|
||||||
|
<option value="credit">Credit</option>
|
||||||
|
<option value="debit">Debit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Amount</label>
|
||||||
|
<input type="number" step="0.01" v-model="form.amount" class="w-full border rounded px-3 py-2" :class="form.errors.amount && 'border-red-500'" />
|
||||||
|
<div v-if="form.errors.amount" class="text-xs text-red-600 mt-1">{{ form.errors.amount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Booked at</label>
|
||||||
|
<input type="datetime-local" v-model="form.booked_at" class="w-full border rounded px-3 py-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Description</label>
|
||||||
|
<input type="text" v-model="form.description" class="w-full border rounded px-3 py-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-2 mt-5">
|
||||||
|
<button class="px-3 py-1.5 border rounded" @click="closeCreate" :disabled="form.processing">Cancel</button>
|
||||||
|
<button class="px-3 py-1.5 rounded text-white bg-emerald-600 disabled:opacity-60" @click="submit" :disabled="form.processing">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
115
resources/js/Pages/Accounts/Payments/Index.vue
Normal file
115
resources/js/Pages/Accounts/Payments/Index.vue
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script setup>
|
||||||
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
|
import { useForm, Link } from '@inertiajs/vue3'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
account: Object,
|
||||||
|
payments: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const showCreate = ref(false)
|
||||||
|
const form = useForm({
|
||||||
|
amount: '',
|
||||||
|
currency: 'EUR',
|
||||||
|
reference: '',
|
||||||
|
paid_at: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
form.reset()
|
||||||
|
form.clearErrors()
|
||||||
|
showCreate.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreate() {
|
||||||
|
showCreate.value = false
|
||||||
|
form.clearErrors()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
form.post(route('accounts.payments.store', { account: props.account.id }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => closeCreate(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppLayout :title="`Payments - ${account.reference || account.id}`">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Payments · {{ account.reference || account.id }}</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Link :href="route('accounts.bookings.index', { account: account.id })" class="text-sm underline">Bookings</Link>
|
||||||
|
<button class="px-3 py-2 bg-emerald-600 text-white rounded" @click="openCreate">New Payment</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||||
|
<table class="min-w-full text-left text-sm">
|
||||||
|
<thead class="border-b text-gray-500">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 pr-4">Paid at</th>
|
||||||
|
<th class="py-2 pr-4">Reference</th>
|
||||||
|
<th class="py-2 pr-4">Amount</th>
|
||||||
|
<th class="py-2 pr-0 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="p in payments" :key="p.id" class="border-b last:border-0">
|
||||||
|
<td class="py-2 pr-4">{{ p.paid_at ? new Date(p.paid_at).toLocaleString() : '-' }}</td>
|
||||||
|
<td class="py-2 pr-4">{{ p.reference || '-' }}</td>
|
||||||
|
<td class="py-2 pr-4 font-medium">{{ Number(p.amount).toFixed(2) }} {{ p.currency }}</td>
|
||||||
|
<td class="py-2 pr-0 text-right">
|
||||||
|
<form :action="route('accounts.payments.destroy', { account: account.id, payment: p.id })" method="post" @submit.prevent="$inertia.delete(route('accounts.payments.destroy', { account: account.id, payment: p.id }), { preserveScroll: true })">
|
||||||
|
<button type="submit" class="text-red-600 hover:underline">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!payments?.length">
|
||||||
|
<td colspan="4" class="py-6 text-center text-gray-500">No payments.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showCreate" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div class="absolute inset-0 bg-black/30" @click="closeCreate"></div>
|
||||||
|
<div class="relative bg-white rounded shadow-lg w-[32rem] max-w-[90%] p-5">
|
||||||
|
<div class="text-lg font-semibold mb-2">New Payment</div>
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Amount</label>
|
||||||
|
<input type="number" step="0.01" v-model="form.amount" class="w-full border rounded px-3 py-2" :class="form.errors.amount && 'border-red-500'" />
|
||||||
|
<div v-if="form.errors.amount" class="text-xs text-red-600 mt-1">{{ form.errors.amount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Currency</label>
|
||||||
|
<input type="text" maxlength="3" v-model="form.currency" class="w-full border rounded px-3 py-2 uppercase" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Paid at</label>
|
||||||
|
<input type="datetime-local" v-model="form.paid_at" class="w-full border rounded px-3 py-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Reference</label>
|
||||||
|
<input type="text" v-model="form.reference" class="w-full border rounded px-3 py-2" :class="form.errors.reference && 'border-red-500'" />
|
||||||
|
<div v-if="form.errors.reference" class="text-xs text-red-600 mt-1">{{ form.errors.reference }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-2 mt-5">
|
||||||
|
<button class="px-3 py-1.5 border rounded" @click="closeCreate" :disabled="form.processing">Cancel</button>
|
||||||
|
<button class="px-3 py-1.5 rounded text-white bg-emerald-600 disabled:opacity-60" @click="submit" :disabled="form.processing">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
|
import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
|
||||||
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
||||||
|
import PaymentDialog from "./PaymentDialog.vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import {
|
import {
|
||||||
faCircleInfo,
|
faCircleInfo,
|
||||||
|
|
@ -48,7 +49,8 @@ const onAddActivity = (c) => emit("add-activity", c);
|
||||||
|
|
||||||
// CaseObject dialog state
|
// CaseObject dialog state
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { router } from "@inertiajs/vue3";
|
import { router, useForm } from "@inertiajs/vue3";
|
||||||
|
import axios from "axios";
|
||||||
const showObjectDialog = ref(false);
|
const showObjectDialog = ref(false);
|
||||||
const showObjectsList = ref(false);
|
const showObjectsList = ref(false);
|
||||||
const selectedContract = ref(null);
|
const selectedContract = ref(null);
|
||||||
|
|
@ -144,6 +146,81 @@ const doChangeSegment = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add Payment modal state
|
||||||
|
const showPaymentDialog = ref(false);
|
||||||
|
const paymentContract = ref(null);
|
||||||
|
const paymentForm = useForm({
|
||||||
|
amount: null,
|
||||||
|
currency: "EUR",
|
||||||
|
paid_at: null,
|
||||||
|
reference: "",
|
||||||
|
});
|
||||||
|
const openPaymentDialog = (c) => {
|
||||||
|
paymentContract.value = c;
|
||||||
|
paymentForm.reset();
|
||||||
|
paymentForm.paid_at = todayStr.value;
|
||||||
|
showPaymentDialog.value = true;
|
||||||
|
};
|
||||||
|
const closePaymentDialog = () => {
|
||||||
|
showPaymentDialog.value = false;
|
||||||
|
paymentContract.value = null;
|
||||||
|
};
|
||||||
|
const submitPayment = () => {
|
||||||
|
if (!paymentContract.value?.account?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const accountId = paymentContract.value.account.id;
|
||||||
|
paymentForm.post(route("accounts.payments.store", { account: accountId }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
closePaymentDialog();
|
||||||
|
// Reload contracts and activities (new payment may create an activity)
|
||||||
|
router.reload({ only: ["contracts", "activities"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// View Payments dialog state and logic
|
||||||
|
const showPaymentsDialog = ref(false);
|
||||||
|
const paymentsForContract = ref([]);
|
||||||
|
const paymentsLoading = ref(false);
|
||||||
|
const openPaymentsDialog = async (c) => {
|
||||||
|
selectedContract.value = c;
|
||||||
|
showPaymentsDialog.value = true;
|
||||||
|
await loadPayments();
|
||||||
|
};
|
||||||
|
const closePaymentsDialog = () => {
|
||||||
|
showPaymentsDialog.value = false;
|
||||||
|
selectedContract.value = null;
|
||||||
|
paymentsForContract.value = [];
|
||||||
|
};
|
||||||
|
const loadPayments = async () => {
|
||||||
|
if (!selectedContract.value?.account?.id) return;
|
||||||
|
paymentsLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(route("accounts.payments.list", { account: selectedContract.value.account.id }));
|
||||||
|
paymentsForContract.value = data.payments || [];
|
||||||
|
} finally {
|
||||||
|
paymentsLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deletePayment = (paymentId) => {
|
||||||
|
if (!selectedContract.value?.account?.id) return;
|
||||||
|
const accountId = selectedContract.value.account.id;
|
||||||
|
router.delete(route("accounts.payments.destroy", { account: accountId, payment: paymentId }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
preserveState: true,
|
||||||
|
only: ["contracts", "activities"],
|
||||||
|
onSuccess: async () => {
|
||||||
|
await loadPayments();
|
||||||
|
},
|
||||||
|
onError: async () => {
|
||||||
|
// Even if there is an error, try to refresh payments list
|
||||||
|
await loadPayments();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -399,6 +476,15 @@ const doChangeSegment = () => {
|
||||||
<span>Briši</span>
|
<span>Briši</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="my-1 border-t border-gray-100" />
|
<div class="my-1 border-t border-gray-100" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||||
|
@click="openPaymentsDialog(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Plačila</span>
|
||||||
|
</button>
|
||||||
|
<div class="my-1 border-t border-gray-100" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||||
|
|
@ -407,6 +493,15 @@ const doChangeSegment = () => {
|
||||||
<FontAwesomeIcon :icon="faListCheck" class="h-4 w-4 text-gray-600" />
|
<FontAwesomeIcon :icon="faListCheck" class="h-4 w-4 text-gray-600" />
|
||||||
<span>Aktivnost</span>
|
<span>Aktivnost</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="my-1 border-t border-gray-100" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||||
|
@click="openPaymentDialog(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Dodaj plačilo</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</FwbTableCell>
|
</FwbTableCell>
|
||||||
|
|
@ -460,4 +555,55 @@ const doChangeSegment = () => {
|
||||||
:client_case="client_case"
|
:client_case="client_case"
|
||||||
:contract="selectedContract"
|
:contract="selectedContract"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PaymentDialog :show="showPaymentDialog" :form="paymentForm" @close="closePaymentDialog" @submit="submitPayment" />
|
||||||
|
|
||||||
|
<!-- View Payments Dialog -->
|
||||||
|
<div v-if="showPaymentsDialog" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-4 w-full max-w-lg">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-base font-medium text-gray-800">
|
||||||
|
Plačila za pogodbo
|
||||||
|
<span class="text-gray-600">{{ selectedContract?.reference }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="text-sm text-gray-500 hover:text-gray-700" @click="closePaymentsDialog">Zapri</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div v-if="paymentsLoading" class="text-sm text-gray-500">Nalaganje…</div>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="paymentsForContract.length === 0" class="text-sm text-gray-500">Ni plačil.</div>
|
||||||
|
<div v-else class="divide-y divide-gray-100 border rounded">
|
||||||
|
<div v-for="p in paymentsForContract" :key="p.id" class="px-3 py-2 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-800">
|
||||||
|
{{
|
||||||
|
Intl.NumberFormat('de-DE', { style: 'currency', currency: p.currency || 'EUR' }).format(p.amount ?? 0)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
<span>{{ formatDate(p.paid_at) }}</span>
|
||||||
|
<span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50"
|
||||||
|
@click="deletePayment(p.id)"
|
||||||
|
title="Izbriši plačilo"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4 text-red-600" />
|
||||||
|
<span class="text-sm">Briši</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
|
<button type="button" class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="loadPayments">Osveži</button>
|
||||||
|
<button type="button" class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" @click="closePaymentsDialog">Zapri</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
78
resources/js/Pages/Cases/Partials/PaymentDialog.vue
Normal file
78
resources/js/Pages/Cases/Partials/PaymentDialog.vue
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup>
|
||||||
|
import DialogModal from "@/Components/DialogModal.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
form: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close", "submit"]);
|
||||||
|
|
||||||
|
const onClose = () => emit("close");
|
||||||
|
const onSubmit = () => emit("submit");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="onClose">
|
||||||
|
<template #title>Dodaj plačilo</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Znesek</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
v-model.number="form.amount"
|
||||||
|
class="w-full rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<div v-if="form.errors?.amount" class="text-sm text-red-600 mt-0.5">
|
||||||
|
{{ form.errors.amount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Valuta</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="form.currency"
|
||||||
|
class="w-full rounded border-gray-300"
|
||||||
|
maxlength="3"
|
||||||
|
/>
|
||||||
|
<div v-if="form.errors?.currency" class="text-sm text-red-600 mt-0.5">
|
||||||
|
{{ form.errors.currency }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Datum</label>
|
||||||
|
<input type="date" v-model="form.paid_at" class="w-full rounded border-gray-300" />
|
||||||
|
<div v-if="form.errors?.paid_at" class="text-sm text-red-600 mt-0.5">
|
||||||
|
{{ form.errors.paid_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Sklic</label>
|
||||||
|
<input type="text" v-model="form.reference" class="w-full rounded border-gray-300" />
|
||||||
|
<div v-if="form.errors?.reference" class="text-sm text-red-600 mt-0.5">
|
||||||
|
{{ form.errors.reference }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button type="button" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="onClose">
|
||||||
|
Prekliči
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
|
||||||
|
:disabled="form.processing"
|
||||||
|
@click="onSubmit"
|
||||||
|
>
|
||||||
|
Shrani
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
@ -14,6 +14,11 @@ import { Link } from '@inertiajs/vue3';
|
||||||
<p class="text-sm text-gray-600 mb-4">Manage segments used across the app.</p>
|
<p class="text-sm text-gray-600 mb-4">Manage segments used across the app.</p>
|
||||||
<Link :href="route('settings.segments')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Segments</Link>
|
<Link :href="route('settings.segments')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Segments</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Payments</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Defaults for payments and auto-activity.</p>
|
||||||
|
<Link :href="route('settings.payment.edit')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Payment Settings</Link>
|
||||||
|
</div>
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||||
<h3 class="text-lg font-semibold mb-2">Workflow</h3>
|
<h3 class="text-lg font-semibold mb-2">Workflow</h3>
|
||||||
<p class="text-sm text-gray-600 mb-4">Configure actions and decisions relationships.</p>
|
<p class="text-sm text-gray-600 mb-4">Configure actions and decisions relationships.</p>
|
||||||
|
|
|
||||||
103
resources/js/Pages/Settings/Payments/Index.vue
Normal file
103
resources/js/Pages/Settings/Payments/Index.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<script setup>
|
||||||
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
|
import { useForm } from '@inertiajs/vue3'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
setting: Object,
|
||||||
|
decisions: Array,
|
||||||
|
actions: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
default_currency: props.setting?.default_currency ?? 'EUR',
|
||||||
|
create_activity_on_payment: !!props.setting?.create_activity_on_payment,
|
||||||
|
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 ?? 'Prejeto plačilo: {amount} {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 {
|
||||||
|
// If current decision not in filtered list, clear it
|
||||||
|
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.payment.update'), {
|
||||||
|
preserveScroll: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppLayout title="Nastavitve plačil">
|
||||||
|
<template #header></template>
|
||||||
|
<div class="max-w-3xl mx-auto p-6">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">Nastavitve plačil</h1>
|
||||||
|
|
||||||
|
<div class="mt-6 grid gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Privzeta valuta</label>
|
||||||
|
<input type="text" maxlength="3" v-model="form.default_currency" class="w-40 rounded border-gray-300" />
|
||||||
|
<div v-if="form.errors.default_currency" class="text-sm text-red-600 mt-1">{{ form.errors.default_currency }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<input type="checkbox" v-model="form.create_activity_on_payment" />
|
||||||
|
<span class="text-sm text-gray-700">Ustvari aktivnost ob dodanem plačilu</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Privzeto dejanje</label>
|
||||||
|
<select v-model="form.default_action_id" class="w-full rounded border-gray-300">
|
||||||
|
<option :value="null">— Brez —</option>
|
||||||
|
<option v-for="a in actions" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||||
|
</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>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Privzeta odločitev</label>
|
||||||
|
<select v-model="form.default_decision_id" class="w-full rounded border-gray-300" :disabled="!form.default_action_id">
|
||||||
|
<option :value="null">— Najprej izberite dejanje —</option>
|
||||||
|
<option v-for="d in filteredDecisions" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||||
|
</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>
|
||||||
|
<label class="block text-sm text-gray-700 mb-1">Predloga opombe aktivnosti</label>
|
||||||
|
<input type="text" v-model="form.activity_note_template" class="w-full rounded border-gray-300" />
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Podprti žetoni: {amount}, {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 type="button" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="form.reset()">Ponastavi</button>
|
||||||
|
<button type="button" class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" :disabled="form.processing" @click="submit">Shrani</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
|
|
@ -68,4 +68,10 @@
|
||||||
Breadcrumbs::for('settings.fieldjob.index', function (BreadcrumbTrail $trail) {
|
Breadcrumbs::for('settings.fieldjob.index', function (BreadcrumbTrail $trail) {
|
||||||
$trail->parent('settings');
|
$trail->parent('settings');
|
||||||
$trail->push('Terensko delo', route('settings.fieldjob.index'));
|
$trail->push('Terensko delo', route('settings.fieldjob.index'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dashboard > Settings > Payments
|
||||||
|
Breadcrumbs::for('settings.payment.edit', function (BreadcrumbTrail $trail) {
|
||||||
|
$trail->parent('settings');
|
||||||
|
$trail->push('Plačila', route('settings.payment.edit'));
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Charts\ExampleChart;
|
use App\Charts\ExampleChart;
|
||||||
|
use App\Http\Controllers\AccountBookingController;
|
||||||
|
use App\Http\Controllers\AccountPaymentController;
|
||||||
use App\Http\Controllers\CaseObjectController;
|
use App\Http\Controllers\CaseObjectController;
|
||||||
use App\Http\Controllers\ClientCaseContoller;
|
use App\Http\Controllers\ClientCaseContoller;
|
||||||
use App\Http\Controllers\ClientController;
|
use App\Http\Controllers\ClientController;
|
||||||
|
|
@ -9,6 +11,7 @@
|
||||||
use App\Http\Controllers\FieldJobSettingController;
|
use App\Http\Controllers\FieldJobSettingController;
|
||||||
use App\Http\Controllers\ImportController;
|
use App\Http\Controllers\ImportController;
|
||||||
use App\Http\Controllers\ImportTemplateController;
|
use App\Http\Controllers\ImportTemplateController;
|
||||||
|
use App\Http\Controllers\PaymentSettingController;
|
||||||
use App\Http\Controllers\PersonController;
|
use App\Http\Controllers\PersonController;
|
||||||
use App\Http\Controllers\PhoneViewController;
|
use App\Http\Controllers\PhoneViewController;
|
||||||
use App\Http\Controllers\SegmentController;
|
use App\Http\Controllers\SegmentController;
|
||||||
|
|
@ -224,6 +227,22 @@
|
||||||
// Route::put()
|
// Route::put()
|
||||||
// types
|
// types
|
||||||
|
|
||||||
|
// accounts / payments & bookings
|
||||||
|
Route::prefix('accounts/{account}')->name('accounts.')->group(function (): void {
|
||||||
|
Route::get('payments', [AccountPaymentController::class, 'index'])->name('payments.index');
|
||||||
|
Route::get('payments/list', [AccountPaymentController::class, 'list'])->name('payments.list');
|
||||||
|
Route::post('payments', [AccountPaymentController::class, 'store'])->name('payments.store');
|
||||||
|
Route::delete('payments/{payment}', [AccountPaymentController::class, 'destroy'])->name('payments.destroy');
|
||||||
|
|
||||||
|
Route::get('bookings', [AccountBookingController::class, 'index'])->name('bookings.index');
|
||||||
|
Route::post('bookings', [AccountBookingController::class, 'store'])->name('bookings.store');
|
||||||
|
Route::delete('bookings/{booking}', [AccountBookingController::class, 'destroy'])->name('bookings.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
// settings - payment settings
|
||||||
|
Route::get('settings/payment', [PaymentSettingController::class, 'edit'])->name('settings.payment.edit');
|
||||||
|
Route::put('settings/payment', [PaymentSettingController::class, 'update'])->name('settings.payment.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();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user