Changes
This commit is contained in:
parent
7227c888d4
commit
a913cfc381
56
app/Http/Controllers/CaseObjectController.php
Normal file
56
app/Http/Controllers/CaseObjectController.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\CaseObject;
|
||||||
|
use App\Models\ClientCase;
|
||||||
|
use App\Models\Contract;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CaseObjectController extends Controller
|
||||||
|
{
|
||||||
|
public function store(ClientCase $clientCase, string $uuid, Request $request)
|
||||||
|
{
|
||||||
|
$contract = Contract::where('uuid', $uuid)->where('client_case_id', $clientCase->id)->firstOrFail();
|
||||||
|
$validated = $request->validate([
|
||||||
|
'reference' => 'nullable|string|max:125',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string|max:255',
|
||||||
|
'type' => 'nullable|string|max:125',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contract->objects()->create($validated);
|
||||||
|
|
||||||
|
return to_route('clientCase.show', $clientCase)->with('success', 'Object created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(ClientCase $clientCase, int $id, Request $request)
|
||||||
|
{
|
||||||
|
$object = CaseObject::where('id', $id)
|
||||||
|
->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'reference' => 'nullable|string|max:125',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string|max:255',
|
||||||
|
'type' => 'nullable|string|max:125',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$object->update($validated);
|
||||||
|
|
||||||
|
return to_route('clientCase.show', $clientCase)->with('success', 'Object updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(ClientCase $clientCase, int $id)
|
||||||
|
{
|
||||||
|
$object = CaseObject::where('id', $id)
|
||||||
|
->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$object->delete();
|
||||||
|
|
||||||
|
return to_route('clientCase.show', $clientCase)->with('success', 'Object deleted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,13 +98,24 @@ public function storeContract(ClientCase $clientCase, Request $request)
|
||||||
|
|
||||||
\DB::transaction(function() use ($request, $clientCase){
|
\DB::transaction(function() use ($request, $clientCase){
|
||||||
|
|
||||||
//Create contract
|
// Create contract
|
||||||
$clientCase->contracts()->create([
|
$contract = $clientCase->contracts()->create([
|
||||||
'reference' => $request->input('reference'),
|
'reference' => $request->input('reference'),
|
||||||
'start_date' => date('Y-m-d', strtotime($request->input('start_date'))),
|
'start_date' => date('Y-m-d', strtotime($request->input('start_date'))),
|
||||||
'type_id' => $request->input('type_id')
|
'type_id' => $request->input('type_id'),
|
||||||
|
'description' => $request->input('description'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Optionally create/update related account amounts
|
||||||
|
$initial = $request->input('initial_amount');
|
||||||
|
$balance = $request->input('balance_amount');
|
||||||
|
if (!is_null($initial) || !is_null($balance)) {
|
||||||
|
$contract->account()->create([
|
||||||
|
'initial_amount' => $initial ?? 0,
|
||||||
|
'balance_amount' => $balance ?? 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return to_route('clientCase.show', $clientCase);
|
return to_route('clientCase.show', $clientCase);
|
||||||
|
|
@ -117,9 +128,25 @@ public function updateContract(ClientCase $clientCase, String $uuid, Request $re
|
||||||
\DB::transaction(function() use ($request, $contract){
|
\DB::transaction(function() use ($request, $contract){
|
||||||
$contract->update([
|
$contract->update([
|
||||||
'reference' => $request->input('reference'),
|
'reference' => $request->input('reference'),
|
||||||
'type_id' => $request->input('type_id')
|
'type_id' => $request->input('type_id'),
|
||||||
|
'description' => $request->input('description'),
|
||||||
|
'start_date' => $request->filled('start_date') ? date('Y-m-d', strtotime($request->input('start_date'))) : $contract->start_date,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$initial = $request->input('initial_amount');
|
||||||
|
$balance = $request->input('balance_amount');
|
||||||
|
if (!is_null($initial) || !is_null($balance)) {
|
||||||
|
$accountData = [
|
||||||
|
'initial_amount' => $initial ?? 0,
|
||||||
|
'balance_amount' => $balance ?? 0,
|
||||||
|
];
|
||||||
|
if ($contract->account) {
|
||||||
|
$contract->account->update($accountData);
|
||||||
|
} else {
|
||||||
|
$contract->account()->create($accountData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return to_route('clientCase.show', $clientCase);
|
return to_route('clientCase.show', $clientCase);
|
||||||
|
|
@ -132,11 +159,28 @@ public function storeActivity(ClientCase $clientCase, Request $request) {
|
||||||
'amount' => 'nullable|decimal:0,4',
|
'amount' => 'nullable|decimal:0,4',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
'action_id' => 'exists:\App\Models\Action,id',
|
'action_id' => 'exists:\App\Models\Action,id',
|
||||||
'decision_id' => 'exists:\App\Models\Decision,id'
|
'decision_id' => 'exists:\App\Models\Decision,id',
|
||||||
|
'contract_uuid' => 'nullable|uuid',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//Create activity
|
// Map contract_uuid to contract_id within the same client case, if provided
|
||||||
$row = $clientCase->activities()->create($attributes);
|
$contractId = null;
|
||||||
|
if (!empty($attributes['contract_uuid'])) {
|
||||||
|
$contract = $clientCase->contracts()->where('uuid', $attributes['contract_uuid'])->firstOrFail('id');
|
||||||
|
if ($contract) {
|
||||||
|
$contractId = $contract->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create activity
|
||||||
|
$row = $clientCase->activities()->create([
|
||||||
|
'due_date' => $attributes['due_date'] ?? null,
|
||||||
|
'amount' => $attributes['amount'] ?? null,
|
||||||
|
'note' => $attributes['note'] ?? null,
|
||||||
|
'action_id' => $attributes['action_id'],
|
||||||
|
'decision_id' => $attributes['decision_id'],
|
||||||
|
'contract_id' => $contractId,
|
||||||
|
]);
|
||||||
/*foreach ($activity->decision->events as $e) {
|
/*foreach ($activity->decision->events as $e) {
|
||||||
$class = '\\App\\Events\\' . $e->name;
|
$class = '\\App\\Events\\' . $e->name;
|
||||||
event(new $class($clientCase));
|
event(new $class($clientCase));
|
||||||
|
|
@ -296,9 +340,9 @@ public function show(ClientCase $clientCase)
|
||||||
'client' => $case->client()->with('person', fn($q) => $q->with(['addresses', 'phones']))->firstOrFail(),
|
'client' => $case->client()->with('person', fn($q) => $q->with(['addresses', 'phones']))->firstOrFail(),
|
||||||
'client_case' => $case,
|
'client_case' => $case,
|
||||||
'contracts' => $case->contracts()
|
'contracts' => $case->contracts()
|
||||||
->with(['type'])
|
->with(['type', 'account', 'objects'])
|
||||||
->orderByDesc('created_at')->get(),
|
->orderByDesc('created_at')->get(),
|
||||||
'activities' => $case->activities()->with(['action', 'decision'])
|
'activities' => $case->activities()->with(['action', 'decision', 'contract:id,uuid,reference'])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(20, ['*'], 'activities'),
|
->paginate(20, ['*'], 'activities'),
|
||||||
'documents' => $case->documents()->orderByDesc('created_at')->get(),
|
'documents' => $case->documents()->orderByDesc('created_at')->get(),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ class ImportController extends Controller
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$paginator = Import::query()
|
$paginator = Import::query()
|
||||||
->with(['client:id,uuid', 'template:id,name'])
|
->with([
|
||||||
|
'client:id,uuid,person_id',
|
||||||
|
'client.person:id,uuid,full_name',
|
||||||
|
'template:id,name',
|
||||||
|
])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(15);
|
->paginate(15);
|
||||||
|
|
||||||
|
|
@ -52,8 +56,15 @@ public function index(Request $request)
|
||||||
'original_name' => $imp->original_name,
|
'original_name' => $imp->original_name,
|
||||||
'size' => $imp->size,
|
'size' => $imp->size,
|
||||||
'status' => $imp->status,
|
'status' => $imp->status,
|
||||||
'client' => $imp->client ? [ 'id' => $imp->client_id, 'uuid' => $imp->client->uuid ] : null,
|
'client' => $imp->client ? [
|
||||||
'template' => $imp->template ? [ 'id' => $imp->import_template_id, 'name' => $imp->template->name ] : null,
|
'id' => $imp->client_id,
|
||||||
|
'uuid' => $imp->client->uuid,
|
||||||
|
'person' => $imp->client->person ? [
|
||||||
|
'uuid' => $imp->client->person->uuid,
|
||||||
|
'full_name' => $imp->client->person->full_name,
|
||||||
|
] : null,
|
||||||
|
] : null,
|
||||||
|
'template' => $imp->template ? [ 'id' => $imp->template->id, 'name' => $imp->template->name ] : null,
|
||||||
];
|
];
|
||||||
}, $imports['data']);
|
}, $imports['data']);
|
||||||
|
|
||||||
|
|
@ -316,6 +327,7 @@ public function show(Import $import)
|
||||||
$templates = ImportTemplate::query()
|
$templates = ImportTemplate::query()
|
||||||
->leftJoin('clients', 'clients.id', '=', 'import_templates.client_id')
|
->leftJoin('clients', 'clients.id', '=', 'import_templates.client_id')
|
||||||
->where('import_templates.is_active', true)
|
->where('import_templates.is_active', true)
|
||||||
|
->where('import_templates.id', $import->import_template_id)
|
||||||
->orderBy('import_templates.name')
|
->orderBy('import_templates.name')
|
||||||
->get([
|
->get([
|
||||||
'import_templates.id',
|
'import_templates.id',
|
||||||
|
|
@ -324,18 +336,30 @@ public function show(Import $import)
|
||||||
'import_templates.source_type',
|
'import_templates.source_type',
|
||||||
'import_templates.default_record_type',
|
'import_templates.default_record_type',
|
||||||
'import_templates.client_id',
|
'import_templates.client_id',
|
||||||
DB::raw('clients.uuid as client_uuid'),
|
'clients.uuid as client_uuid',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
$clients = Client::query()
|
$clients = Client::query()
|
||||||
->join('person', 'person.id', '=', 'clients.person_id')
|
->join('person', 'person.id', '=', 'clients.person_id')
|
||||||
->orderBy('person.full_name')
|
->orderBy('person.full_name')
|
||||||
|
->where('clients.id', $import->client_id)
|
||||||
->get([
|
->get([
|
||||||
'clients.id',
|
'clients.id',
|
||||||
'clients.uuid',
|
'clients.uuid',
|
||||||
DB::raw('person.full_name as name'),
|
'person.full_name as name'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Import client
|
||||||
|
$client = Client::query()
|
||||||
|
->join('person', 'person.id', '=', 'clients.person_id')
|
||||||
|
->where('clients.id', $import->client_id)
|
||||||
|
->firstOrFail([
|
||||||
|
'clients.uuid as uuid',
|
||||||
|
'person.full_name as name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
// Render a dedicated page to continue the import
|
// Render a dedicated page to continue the import
|
||||||
return Inertia::render('Imports/Import', [
|
return Inertia::render('Imports/Import', [
|
||||||
'import' => [
|
'import' => [
|
||||||
|
|
@ -344,15 +368,17 @@ public function show(Import $import)
|
||||||
'status' => $import->status,
|
'status' => $import->status,
|
||||||
'meta' => $import->meta,
|
'meta' => $import->meta,
|
||||||
'client_id' => $import->client_id,
|
'client_id' => $import->client_id,
|
||||||
|
'client_uuid' => optional($client)->uuid,
|
||||||
'import_template_id' => $import->import_template_id,
|
'import_template_id' => $import->import_template_id,
|
||||||
'total_rows' => $import->total_rows,
|
'total_rows' => $import->total_rows,
|
||||||
'imported_rows' => $import->imported_rows,
|
'imported_rows' => $import->imported_rows,
|
||||||
'invalid_rows' => $import->invalid_rows,
|
'invalid_rows' => $import->invalid_rows,
|
||||||
'valid_rows' => $import->valid_rows,
|
'valid_rows' => $import->valid_rows,
|
||||||
'finished_at' => $import->finished_at,
|
'finished_at' => $import->finished_at
|
||||||
],
|
],
|
||||||
'templates' => $templates,
|
'templates' => $templates,
|
||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
|
'client' => $client
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Person\Person;
|
use App\Models\Person\Person;
|
||||||
|
use App\Models\BankAccount;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
|
@ -78,6 +79,13 @@ public function updateAddress(Person $person, int $address_id, Request $request)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteAddress(Person $person, int $address_id, Request $request)
|
||||||
|
{
|
||||||
|
$address = $person->addresses()->findOrFail($address_id);
|
||||||
|
$address->delete(); // soft delete
|
||||||
|
return response()->json(['status' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
public function createPhone(Person $person, Request $request)
|
public function createPhone(Person $person, Request $request)
|
||||||
{
|
{
|
||||||
$attributes = $request->validate([
|
$attributes = $request->validate([
|
||||||
|
|
@ -116,6 +124,13 @@ public function updatePhone(Person $person, int $phone_id, Request $request)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deletePhone(Person $person, int $phone_id, Request $request)
|
||||||
|
{
|
||||||
|
$phone = $person->phones()->findOrFail($phone_id);
|
||||||
|
$phone->delete(); // soft delete
|
||||||
|
return response()->json(['status' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
public function createEmail(Person $person, Request $request)
|
public function createEmail(Person $person, Request $request)
|
||||||
{
|
{
|
||||||
$attributes = $request->validate([
|
$attributes = $request->validate([
|
||||||
|
|
@ -160,4 +175,66 @@ public function updateEmail(Person $person, int $email_id, Request $request)
|
||||||
'email' => $email
|
'email' => $email
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteEmail(Person $person, int $email_id, Request $request)
|
||||||
|
{
|
||||||
|
$email = $person->emails()->findOrFail($email_id);
|
||||||
|
$email->delete();
|
||||||
|
return response()->json(['status' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRR (bank account) CRUD
|
||||||
|
public function createTrr(Person $person, Request $request)
|
||||||
|
{
|
||||||
|
$attributes = $request->validate([
|
||||||
|
'iban' => 'nullable|string|max:34',
|
||||||
|
'bank_name' => 'required|string|max:100',
|
||||||
|
'bic_swift' => 'nullable|string|max:11',
|
||||||
|
'account_number' => 'nullable|string|max:34',
|
||||||
|
'routing_number' => 'nullable|string|max:20',
|
||||||
|
'currency' => 'required|string|size:3',
|
||||||
|
'country_code' => 'nullable|string|size:2',
|
||||||
|
'holder_name' => 'nullable|string|max:125',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'meta' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided
|
||||||
|
$trr = $person->bankAccounts()->create($attributes);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'trr' => BankAccount::findOrFail($trr->id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTrr(Person $person, int $trr_id, Request $request)
|
||||||
|
{
|
||||||
|
$attributes = $request->validate([
|
||||||
|
'iban' => 'nullable|string|max:34',
|
||||||
|
'bank_name' => 'required|string|max:100',
|
||||||
|
'bic_swift' => 'nullable|string|max:11',
|
||||||
|
'account_number' => 'nullable|string|max:34',
|
||||||
|
'routing_number' => 'nullable|string|max:20',
|
||||||
|
'currency' => 'required|string|size:3',
|
||||||
|
'country_code' => 'nullable|string|size:2',
|
||||||
|
'holder_name' => 'nullable|string|max:125',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'meta' => 'nullable|array',
|
||||||
|
'is_active' => 'sometimes|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
||||||
|
$trr->update($attributes);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'trr' => $trr
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteTrr(Person $person, int $trr_id, Request $request)
|
||||||
|
{
|
||||||
|
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
||||||
|
$trr->delete();
|
||||||
|
return response()->json(['status' => 'ok']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class Account extends Model
|
||||||
'contract_id',
|
'contract_id',
|
||||||
'type_id',
|
'type_id',
|
||||||
'active',
|
'active',
|
||||||
|
'initial_amount',
|
||||||
'balance_amount',
|
'balance_amount',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ class Activity extends Model
|
||||||
'note',
|
'note',
|
||||||
'action_id',
|
'action_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
'decision_id'
|
'decision_id',
|
||||||
|
'contract_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
|
|
||||||
41
app/Models/BankAccount.php
Normal file
41
app/Models/BankAccount.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?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 BankAccount extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'bank_accounts';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'person_id',
|
||||||
|
'bank_name',
|
||||||
|
'iban',
|
||||||
|
'bic_swift',
|
||||||
|
'account_number',
|
||||||
|
'routing_number',
|
||||||
|
'currency',
|
||||||
|
'country_code',
|
||||||
|
'holder_name',
|
||||||
|
'is_active',
|
||||||
|
'notes',
|
||||||
|
'meta',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'meta' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function person(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\Person\Person::class, 'person_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Models/CaseObject.php
Normal file
29
app/Models/CaseObject.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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 CaseObject extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'objects';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'reference',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'type',
|
||||||
|
'contract_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function contract(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Contract::class, 'contract_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
@ -53,4 +54,15 @@ public function segments(): BelongsToMany {
|
||||||
->wherePivot('active', true);
|
->wherePivot('active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function account(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(\App\Models\Account::class)
|
||||||
|
->with('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function objects(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\CaseObject::class, 'contract_id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@
|
||||||
|
|
||||||
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\SoftDeletes;
|
||||||
|
|
||||||
class Email extends Model
|
class Email extends Model
|
||||||
{
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'person_id',
|
'person_id',
|
||||||
'value',
|
'value',
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,13 @@ public function emails(): HasMany
|
||||||
->orderBy('id');
|
->orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bankAccounts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\BankAccount::class, 'person_id')
|
||||||
|
->where('is_active', '=', 1)
|
||||||
|
->orderBy('id');
|
||||||
|
}
|
||||||
|
|
||||||
public function group(): BelongsTo
|
public function group(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\Person\PersonGroup::class, 'group_id');
|
return $this->belongsTo(\App\Models\Person\PersonGroup::class, 'group_id');
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
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\SoftDeletes;
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class PersonAddress extends Model
|
class PersonAddress extends Model
|
||||||
|
|
@ -12,6 +13,7 @@ class PersonAddress extends Model
|
||||||
/** @use HasFactory<\Database\Factories\Person/PersonAddressFactory> */
|
/** @use HasFactory<\Database\Factories\Person/PersonAddressFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Searchable;
|
use Searchable;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'address',
|
'address',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
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\SoftDeletes;
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class PersonPhone extends Model
|
class PersonPhone extends Model
|
||||||
|
|
@ -13,6 +14,7 @@ class PersonPhone extends Model
|
||||||
/** @use HasFactory<\Database\Factories\Person/PersonPhoneFactory> */
|
/** @use HasFactory<\Database\Factories\Person/PersonPhoneFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Searchable;
|
use Searchable;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'nu',
|
'nu',
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,21 @@
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('object_types', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name',50);
|
|
||||||
$table->string('description',125)->nullable();
|
|
||||||
$table->softDeletes();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('objects', function (Blueprint $table) {
|
Schema::create('objects', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('reference', 125)->nullable();
|
$table->string('reference', 125)->nullable();
|
||||||
$table->string('name', 255);
|
$table->string('name', 255);
|
||||||
$table->string('description', 255)->nullable();
|
$table->string('description', 255)->nullable();
|
||||||
// If you keep the column name as 'type_id', specify the table explicitly
|
$table->string('type', 125)->nullable();
|
||||||
$table->foreignId('type_id')->constrained('object_types')->nullOnDelete();
|
$table->foreignId('contract_id')->constrained('contracts')->cascadeOnDelete();
|
||||||
// Indexes for faster lookups
|
// Indexes for faster lookups
|
||||||
$table->softDeletes();
|
$table->softDeletes();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('type');
|
||||||
$table->index('reference');
|
$table->index('reference');
|
||||||
$table->index('type_id');
|
$table->index('contract_id');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?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::dropIfExists('objects');
|
||||||
|
Schema::dropIfExists('object_types');
|
||||||
|
|
||||||
|
Schema::create('objects', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('reference', 125)->nullable();
|
||||||
|
$table->string('name', 255);
|
||||||
|
$table->string('description', 255)->nullable();
|
||||||
|
$table->string('type', 125)->nullable();
|
||||||
|
$table->foreignId('contract_id')->constrained('contracts')->cascadeOnDelete();
|
||||||
|
// Indexes for faster lookups
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('type');
|
||||||
|
$table->index('reference');
|
||||||
|
$table->index('contract_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('objects');
|
||||||
|
Schema::dropIfExists('object_types');
|
||||||
|
}
|
||||||
|
};
|
||||||
779
package-lock.json
generated
779
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -35,6 +35,7 @@
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"preline": "^2.7.0",
|
"preline": "^2.7.0",
|
||||||
"tailwindcss-inner-border": "^0.2.0",
|
"tailwindcss-inner-border": "^0.2.0",
|
||||||
|
"v-calendar": "^3.1.2",
|
||||||
"vue-multiselect": "^3.1.0",
|
"vue-multiselect": "^3.1.0",
|
||||||
"vue-search-input": "^1.1.16",
|
"vue-search-input": "^1.1.16",
|
||||||
"vue3-apexcharts": "^1.7.0",
|
"vue3-apexcharts": "^1.7.0",
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,6 @@
|
||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure dropdowns/menus render above dialog overlays when appended to body */
|
||||||
|
.multiselect__content-wrapper { z-index: 2147483647 !important; }
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,16 @@ const props = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: {}
|
default: {}
|
||||||
|
},
|
||||||
|
// Deprecated: fixed height. Prefer bodyMaxHeight (e.g., 'max-h-96').
|
||||||
|
bodyHeight: {
|
||||||
|
type: String,
|
||||||
|
default: 'h-96'
|
||||||
|
},
|
||||||
|
// Preferred: control scrollable body max-height (Tailwind class), e.g., 'max-h-96', 'max-h-[600px]'
|
||||||
|
bodyMaxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: 'max-h-96'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -105,7 +115,7 @@ const remove = () => {
|
||||||
<p v-if="description" class="mt-1 text-sm text-gray-600">{{ description }}</p>
|
<p v-if="description" class="mt-1 text-sm text-gray-600">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
|
<div :class="['relative rounded-lg border border-gray-200 bg-white shadow-sm overflow-x-auto overflow-y-auto', bodyMaxHeight]">
|
||||||
<FwbTable hoverable striped class="text-sm">
|
<FwbTable hoverable striped class="text-sm">
|
||||||
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur supports-[backdrop-filter]:bg-gray-50/80 border-b border-gray-200 shadow-sm">
|
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur supports-[backdrop-filter]:bg-gray-50/80 border-b border-gray-200 shadow-sm">
|
||||||
<FwbTableHeadCell v-for="(h, hIndex) in header" :key="hIndex" class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6">{{ h.data }}</FwbTableHeadCell>
|
<FwbTableHeadCell v-for="(h, hIndex) in header" :key="hIndex" class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6">{{ h.data }}</FwbTableHeadCell>
|
||||||
|
|
|
||||||
38
resources/js/Components/ConfirmDialog.vue
Normal file
38
resources/js/Components/ConfirmDialog.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script setup>
|
||||||
|
import DialogModal from './DialogModal.vue';
|
||||||
|
import PrimaryButton from './PrimaryButton.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
title: { type: String, default: 'Potrditev' },
|
||||||
|
message: { type: String, default: 'Ali ste prepričani?' },
|
||||||
|
confirmText: { type: String, default: 'Potrdi' },
|
||||||
|
cancelText: { type: String, default: 'Prekliči' },
|
||||||
|
danger: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'confirm']);
|
||||||
|
|
||||||
|
const onClose = () => emit('close');
|
||||||
|
const onConfirm = () => emit('confirm');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="onClose">
|
||||||
|
<template #title>
|
||||||
|
{{ title }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<p class="text-sm text-gray-700">{{ message }}</p>
|
||||||
|
<div class="mt-6 flex items-center justify-end gap-3">
|
||||||
|
<button type="button" class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50" @click="onClose">
|
||||||
|
{{ cancelText }}
|
||||||
|
</button>
|
||||||
|
<PrimaryButton :class="danger ? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' : ''" @click="onConfirm">
|
||||||
|
{{ confirmText }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
|
||||||
|
</template>
|
||||||
103
resources/js/Components/DatePickerField.vue
Normal file
103
resources/js/Components/DatePickerField.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<script setup>
|
||||||
|
import InputLabel from './InputLabel.vue'
|
||||||
|
import InputError from './InputError.vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/*
|
||||||
|
DatePickerField (v-calendar)
|
||||||
|
- A thin wrapper around <VDatePicker> with a label and error support.
|
||||||
|
- Uses v-calendar which handles popovers/teleport well inside modals.
|
||||||
|
API: kept compatible with previous usage where possible.
|
||||||
|
Props:
|
||||||
|
- modelValue: Date | string | number | null
|
||||||
|
- id: string
|
||||||
|
- label: string
|
||||||
|
- format: string (default 'dd.MM.yyyy')
|
||||||
|
- enableTimePicker: boolean (default false)
|
||||||
|
- inline: boolean (default false) // When true, keeps the popover visible
|
||||||
|
- placeholder: string
|
||||||
|
- error: string | string[]
|
||||||
|
Note: Props like teleportTarget/autoPosition/menuClassName/fixed/closeOn... were for the old picker
|
||||||
|
and are accepted for compatibility but are not used by v-calendar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [Date, String, Number, null], default: null },
|
||||||
|
id: { type: String, default: undefined },
|
||||||
|
label: { type: String, default: undefined },
|
||||||
|
format: { type: String, default: 'dd.MM.yyyy' },
|
||||||
|
enableTimePicker: { type: Boolean, default: false },
|
||||||
|
inline: { type: Boolean, default: false },
|
||||||
|
// legacy/unused in v-calendar (kept to prevent breaking callers)
|
||||||
|
autoApply: { type: Boolean, default: false },
|
||||||
|
teleportTarget: { type: [Boolean, String], default: 'body' },
|
||||||
|
autoPosition: { type: Boolean, default: true },
|
||||||
|
menuClassName: { type: String, default: 'dp-over-modal' },
|
||||||
|
fixed: { type: Boolean, default: true },
|
||||||
|
closeOnAutoApply: { type: Boolean, default: true },
|
||||||
|
closeOnScroll: { type: Boolean, default: true },
|
||||||
|
placeholder: { type: String, default: '' },
|
||||||
|
error: { type: [String, Array], default: undefined },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const valueProxy = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
emit('change', val)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert common date mask from lowercase tokens to v-calendar tokens
|
||||||
|
const inputMask = computed(() => {
|
||||||
|
let m = props.format || 'dd.MM.yyyy'
|
||||||
|
return m
|
||||||
|
.replace(/yyyy/g, 'YYYY')
|
||||||
|
.replace(/dd/g, 'DD')
|
||||||
|
.replace(/MM/g, 'MM')
|
||||||
|
+ (props.enableTimePicker ? ' HH:mm' : '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const popoverCfg = computed(() => ({
|
||||||
|
visibility: props.inline ? 'visible' : 'click',
|
||||||
|
placement: 'bottom-start',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel v-if="label" :for="id" :value="label" />
|
||||||
|
|
||||||
|
<!-- VCalendar DatePicker with custom input to keep Tailwind styling -->
|
||||||
|
<VDatePicker
|
||||||
|
v-model="valueProxy"
|
||||||
|
:mode="enableTimePicker ? 'dateTime' : 'date'"
|
||||||
|
:masks="{ input: inputMask }"
|
||||||
|
:popover="popoverCfg"
|
||||||
|
:is24hr="true"
|
||||||
|
>
|
||||||
|
<template #default="{ inputValue, inputEvents }">
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:value="inputValue"
|
||||||
|
v-on="inputEvents"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VDatePicker>
|
||||||
|
|
||||||
|
<template v-if="error">
|
||||||
|
<InputError v-if="Array.isArray(error)" v-for="(e, idx) in error" :key="idx" :message="e" />
|
||||||
|
<InputError v-else :message="error" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Ensure the date picker menu overlays modals/dialogs */
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell, FwbBadge } from 'flowbite-vue'
|
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell, FwbBadge } from 'flowbite-vue'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { faFilePdf, faFileWord, faFileExcel, faFileLines, faFileImage, faFile, faCircleInfo } from '@fortawesome/free-solid-svg-icons'
|
import { faFilePdf, faFileWord, faFileExcel, faFileLines, faFileImage, faFile, faCircleInfo, faEllipsisVertical, faDownload } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import Dropdown from '@/Components/Dropdown.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
documents: { type: Array, default: () => [] },
|
documents: { type: Array, default: () => [] },
|
||||||
viewUrlBuilder: { type: Function, default: null },
|
viewUrlBuilder: { type: Function, default: null },
|
||||||
|
// Optional: build a direct download URL for a document; if not provided, a 'download' event will be emitted
|
||||||
|
downloadUrlBuilder: { type: Function, default: null },
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['view'])
|
const emit = defineEmits(['view', 'download'])
|
||||||
|
|
||||||
const formatSize = (bytes) => {
|
const formatSize = (bytes) => {
|
||||||
if (bytes == null) return '-'
|
if (bytes == null) return '-'
|
||||||
|
|
@ -78,6 +81,29 @@ const toggleDesc = (doc, i) => {
|
||||||
const key = rowKey(doc, i)
|
const key = rowKey(doc, i)
|
||||||
expandedDescKey.value = expandedDescKey.value === key ? null : key
|
expandedDescKey.value = expandedDescKey.value === key ? null : key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const resolveDownloadUrl = (doc) => {
|
||||||
|
if (typeof props.downloadUrlBuilder === 'function') return props.downloadUrlBuilder(doc)
|
||||||
|
// If no builder provided, parent can handle via emitted event
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = (doc) => {
|
||||||
|
const url = resolveDownloadUrl(doc)
|
||||||
|
if (url) {
|
||||||
|
// Trigger a navigation to a direct-download endpoint; server should set Content-Disposition: attachment
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.target = '_self'
|
||||||
|
a.rel = 'noopener'
|
||||||
|
// In many browsers, simply setting href is enough
|
||||||
|
a.click()
|
||||||
|
} else {
|
||||||
|
emit('download', doc)
|
||||||
|
}
|
||||||
|
closeActions()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -120,7 +146,28 @@ const toggleDesc = (doc, i) => {
|
||||||
</button>
|
</button>
|
||||||
</FwbTableCell>
|
</FwbTableCell>
|
||||||
<FwbTableCell class="text-right whitespace-nowrap">
|
<FwbTableCell class="text-right whitespace-nowrap">
|
||||||
<!-- future actions: download/delete -->
|
<Dropdown align="right" width="48">
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
:title="'Actions'"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<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="handleDownload(doc)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Download file</span>
|
||||||
|
</button>
|
||||||
|
<!-- future actions can be slotted here -->
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
</FwbTableCell>
|
</FwbTableCell>
|
||||||
</FwbTableRow>
|
</FwbTableRow>
|
||||||
<!-- Expanded description row directly below the item -->
|
<!-- Expanded description row directly below the item -->
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
align: {
|
align: {
|
||||||
|
|
@ -17,6 +17,9 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
let open = ref(false);
|
let open = ref(false);
|
||||||
|
const triggerEl = ref(null);
|
||||||
|
const panelEl = ref(null);
|
||||||
|
const panelStyle = ref({ top: '0px', left: '0px' });
|
||||||
|
|
||||||
const closeOnEscape = (e) => {
|
const closeOnEscape = (e) => {
|
||||||
if (open.value && e.key === 'Escape') {
|
if (open.value && e.key === 'Escape') {
|
||||||
|
|
@ -24,8 +27,54 @@ const closeOnEscape = (e) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
const t = triggerEl.value;
|
||||||
|
const p = panelEl.value;
|
||||||
|
if (!t || !p) return;
|
||||||
|
const rect = t.getBoundingClientRect();
|
||||||
|
// Ensure we have updated width
|
||||||
|
const pw = p.offsetWidth || 0;
|
||||||
|
const ph = p.offsetHeight || 0;
|
||||||
|
const margin = 8; // small spacing from trigger
|
||||||
|
let left = rect.left;
|
||||||
|
if (props.align === 'right') {
|
||||||
|
left = rect.right - pw;
|
||||||
|
} else if (props.align === 'left') {
|
||||||
|
left = rect.left;
|
||||||
|
}
|
||||||
|
// Clamp within viewport
|
||||||
|
const maxLeft = Math.max(0, window.innerWidth - pw - margin);
|
||||||
|
left = Math.min(Math.max(margin, left), maxLeft);
|
||||||
|
let top = rect.bottom + margin;
|
||||||
|
// If not enough space below, place above the trigger
|
||||||
|
if (top + ph > window.innerHeight) {
|
||||||
|
top = Math.max(margin, rect.top - ph - margin);
|
||||||
|
}
|
||||||
|
panelStyle.value = { top: `${top}px`, left: `${left}px` };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWindowChange = () => {
|
||||||
|
updatePosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(open, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
updatePosition();
|
||||||
|
window.addEventListener('resize', onWindowChange);
|
||||||
|
window.addEventListener('scroll', onWindowChange, true);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('resize', onWindowChange);
|
||||||
|
window.removeEventListener('scroll', onWindowChange, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||||
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', closeOnEscape);
|
||||||
|
window.removeEventListener('resize', onWindowChange);
|
||||||
|
window.removeEventListener('scroll', onWindowChange, true);
|
||||||
|
});
|
||||||
|
|
||||||
const widthClass = computed(() => {
|
const widthClass = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -47,33 +96,35 @@ const alignmentClasses = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative" ref="triggerEl">
|
||||||
<div @click="open = ! open">
|
<div @click="open = ! open">
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Full Screen Dropdown Overlay -->
|
<teleport to="body">
|
||||||
<div v-show="open" class="fixed inset-0 z-40" @click="open = false" />
|
<!-- Full Screen Dropdown Overlay at body level -->
|
||||||
|
<div v-show="open" class="fixed inset-0 z-[2147483646]" @click="open = false" />
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition ease-out duration-200"
|
enter-active-class="transition ease-out duration-200"
|
||||||
enter-from-class="transform opacity-0 scale-95"
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
enter-to-class="transform opacity-100 scale-100"
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
leave-active-class="transition ease-in duration-75"
|
leave-active-class="transition ease-in duration-75"
|
||||||
leave-from-class="transform opacity-100 scale-100"
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
leave-to-class="transform opacity-0 scale-95"
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-show="open"
|
|
||||||
class="absolute z-50 mt-2 rounded-md shadow-lg"
|
|
||||||
:class="[widthClass, alignmentClasses]"
|
|
||||||
style="display: none;"
|
|
||||||
@click="open = false"
|
|
||||||
>
|
>
|
||||||
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses">
|
<div
|
||||||
<slot name="content" />
|
v-show="open"
|
||||||
|
ref="panelEl"
|
||||||
|
class="fixed z-[2147483647] rounded-md shadow-lg"
|
||||||
|
:class="[widthClass]"
|
||||||
|
:style="[panelStyle]"
|
||||||
|
>
|
||||||
|
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses" @click="open = false">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition>
|
||||||
</transition>
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
120
resources/js/Components/EmailCreateForm.vue
Normal file
120
resources/js/Components/EmailCreateForm.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import DialogModal from './DialogModal.vue';
|
||||||
|
import InputLabel from './InputLabel.vue';
|
||||||
|
import SectionTitle from './SectionTitle.vue';
|
||||||
|
import TextInput from './TextInput.vue';
|
||||||
|
import InputError from './InputError.vue';
|
||||||
|
import PrimaryButton from './PrimaryButton.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/*
|
||||||
|
EmailCreateForm / Email editor
|
||||||
|
- Props mirror Phone/Address forms for consistency
|
||||||
|
- Routes assumed: person.email.create, person.email.update
|
||||||
|
- Adjust route names/fields to match your backend if different
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
person: { type: Object, required: true },
|
||||||
|
edit: { type: Boolean, default: false },
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const processing = ref(false);
|
||||||
|
const errors = ref({});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
setTimeout(() => { errors.value = {}; }, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
value: '',
|
||||||
|
label: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = { value: '', label: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
processing.value = true; errors.value = {};
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(route('person.email.create', props.person), form.value);
|
||||||
|
if (!Array.isArray(props.person.emails)) props.person.emails = [];
|
||||||
|
props.person.emails.push(data.email);
|
||||||
|
processing.value = false; close(); resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = e?.response?.data?.errors || {}; processing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
processing.value = true; errors.value = {};
|
||||||
|
try {
|
||||||
|
const { data } = await axios.put(route('person.email.update', { person: props.person, email_id: props.id }), form.value);
|
||||||
|
if (!Array.isArray(props.person.emails)) props.person.emails = [];
|
||||||
|
const idx = props.person.emails.findIndex(e => e.id === data.email.id);
|
||||||
|
if (idx !== -1) props.person.emails[idx] = data.email;
|
||||||
|
processing.value = false; close(); resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = e?.response?.data?.errors || {}; processing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
(id) => {
|
||||||
|
if (props.edit && id) {
|
||||||
|
const current = (props.person.emails || []).find(e => e.id === id);
|
||||||
|
if (current) {
|
||||||
|
form.value = {
|
||||||
|
value: current.value || current.email || current.address || '',
|
||||||
|
label: current.label || ''
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const submit = () => (props.edit ? update() : create());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="close">
|
||||||
|
<template #title>
|
||||||
|
<span v-if="edit">Spremeni email</span>
|
||||||
|
<span v-else>Dodaj email</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<SectionTitle class="border-b mb-4">
|
||||||
|
<template #title>Email</template>
|
||||||
|
</SectionTitle>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="em_value" value="E-pošta" />
|
||||||
|
<TextInput id="em_value" v-model="form.value" type="email" class="mt-1 block w-full" autocomplete="email" />
|
||||||
|
<InputError v-if="errors.value" v-for="err in errors.value" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="em_label" value="Oznaka (neobvezno)" />
|
||||||
|
<TextInput id="em_label" v-model="form.label" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.label" v-for="err in errors.label" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<PrimaryButton :class="{ 'opacity-25': processing }" :disabled="processing">Shrani</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
15
resources/js/Components/EmailUpdateForm.vue
Normal file
15
resources/js/Components/EmailUpdateForm.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup>
|
||||||
|
// This component reuses EmailCreateForm's logic via props.edit=true
|
||||||
|
import EmailCreateForm from './EmailCreateForm.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
person: { type: Object, required: true },
|
||||||
|
types: { type: Array, default: () => [] },
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EmailCreateForm :show="show" :person="person" :types="types" :edit="true" :id="id" @close="$emit('close')" />
|
||||||
|
</template>
|
||||||
|
|
@ -92,7 +92,7 @@ const maxWidthClass = computed(() => {
|
||||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div v-show="show" class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
|
<div v-show="show" class="mb-6 bg-white rounded-lg overflow-visible shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
|
||||||
<slot v-if="showSlot"/>
|
<slot v-if="showSlot"/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FwbBadge } from 'flowbite-vue';
|
import { FwbBadge } from 'flowbite-vue';
|
||||||
import { EditIcon, PlusIcon, UserEditIcon } from '@/Utilities/Icons';
|
import { EditIcon, PlusIcon, UserEditIcon, TrashBinIcon } from '@/Utilities/Icons';
|
||||||
import CusTab from './CusTab.vue';
|
import CusTab from './CusTab.vue';
|
||||||
import CusTabs from './CusTabs.vue';
|
import CusTabs from './CusTabs.vue';
|
||||||
import { provide, ref, watch } from 'vue';
|
import { provide, ref, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
import PersonUpdateForm from './PersonUpdateForm.vue';
|
import PersonUpdateForm from './PersonUpdateForm.vue';
|
||||||
import AddressCreateForm from './AddressCreateForm.vue';
|
import AddressCreateForm from './AddressCreateForm.vue';
|
||||||
import PhoneCreateForm from './PhoneCreateForm.vue';
|
import PhoneCreateForm from './PhoneCreateForm.vue';
|
||||||
|
import EmailCreateForm from './EmailCreateForm.vue';
|
||||||
|
import EmailUpdateForm from './EmailUpdateForm.vue';
|
||||||
|
import TrrCreateForm from './TrrCreateForm.vue';
|
||||||
|
import TrrUpdateForm from './TrrUpdateForm.vue';
|
||||||
|
import ConfirmDialog from './ConfirmDialog.vue';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -32,12 +38,38 @@ const props = defineProps({
|
||||||
const drawerUpdatePerson = ref(false);
|
const drawerUpdatePerson = ref(false);
|
||||||
const drawerAddAddress = ref(false);
|
const drawerAddAddress = ref(false);
|
||||||
const drawerAddPhone = ref(false);
|
const drawerAddPhone = ref(false);
|
||||||
|
const drawerAddEmail = ref(false);
|
||||||
|
const drawerAddTrr = ref(false);
|
||||||
|
|
||||||
const editAddress = ref(false);
|
const editAddress = ref(false);
|
||||||
const editAddressId = ref(0);
|
const editAddressId = ref(0);
|
||||||
|
|
||||||
const editPhone = ref(false);
|
const editPhone = ref(false);
|
||||||
const editPhoneId = ref(0);
|
const editPhoneId = ref(0);
|
||||||
|
const editEmail = ref(false);
|
||||||
|
const editEmailId = ref(0);
|
||||||
|
const editTrr = ref(false);
|
||||||
|
const editTrrId = ref(0);
|
||||||
|
|
||||||
|
// Confirm dialog state
|
||||||
|
const confirm = ref({
|
||||||
|
show: false,
|
||||||
|
title: 'Potrditev brisanja',
|
||||||
|
message: '',
|
||||||
|
type: '', // 'email' | 'trr' | 'address' | 'phone'
|
||||||
|
id: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openConfirm = (type, id, label = '') => {
|
||||||
|
confirm.value = {
|
||||||
|
show: true,
|
||||||
|
title: 'Potrditev brisanja',
|
||||||
|
message: label ? `Ali res želite izbrisati “${label}”?` : 'Ali res želite izbrisati izbran element?',
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const closeConfirm = () => { confirm.value.show = false; };
|
||||||
|
|
||||||
const getMainAddress = (adresses) => {
|
const getMainAddress = (adresses) => {
|
||||||
const addr = adresses.filter( a => a.type.id === 1 )[0] ?? '';
|
const addr = adresses.filter( a => a.type.id === 1 )[0] ?? '';
|
||||||
|
|
@ -77,6 +109,70 @@ const operDrawerAddPhone = (edit = false, id = 0) => {
|
||||||
editPhoneId.value = id;
|
editPhoneId.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openDrawerAddEmail = (edit = false, id = 0) => {
|
||||||
|
drawerAddEmail.value = true;
|
||||||
|
editEmail.value = edit;
|
||||||
|
editEmailId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDrawerAddTrr = (edit = false, id = 0) => {
|
||||||
|
drawerAddTrr.value = true;
|
||||||
|
editTrr.value = edit;
|
||||||
|
editTrrId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete handlers (expects routes: person.email.delete, person.trr.delete)
|
||||||
|
const deleteEmail = async (emailId, label = '') => {
|
||||||
|
if (!emailId) return;
|
||||||
|
openConfirm('email', emailId, label || 'email');
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTrr = async (trrId, label = '') => {
|
||||||
|
if (!trrId) return;
|
||||||
|
openConfirm('trr', trrId, label || 'TRR');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirmDelete = async () => {
|
||||||
|
const { type, id } = confirm.value;
|
||||||
|
try {
|
||||||
|
if (type === 'email') {
|
||||||
|
await axios.delete(route('person.email.delete', { person: props.person, email_id: id }));
|
||||||
|
const list = props.person.emails || [];
|
||||||
|
const idx = list.findIndex(e => e.id === id);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
} else if (type === 'trr') {
|
||||||
|
await axios.delete(route('person.trr.delete', { person: props.person, trr_id: id }));
|
||||||
|
let list = props.person.trrs || props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || [];
|
||||||
|
const idx = list.findIndex(a => a.id === id);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
} else if (type === 'address') {
|
||||||
|
await axios.delete(route('person.address.delete', { person: props.person, address_id: id }));
|
||||||
|
const list = props.person.addresses || [];
|
||||||
|
const idx = list.findIndex(a => a.id === id);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
} else if (type === 'phone') {
|
||||||
|
await axios.delete(route('person.phone.delete', { person: props.person, phone_id: id }));
|
||||||
|
const list = props.person.phones || [];
|
||||||
|
const idx = list.findIndex(p => p.id === id);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
}
|
||||||
|
closeConfirm();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Delete failed', e?.response || e);
|
||||||
|
closeConfirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe accessors for optional collections
|
||||||
|
const getEmails = (p) => Array.isArray(p?.emails) ? p.emails : []
|
||||||
|
const getTRRs = (p) => {
|
||||||
|
if (Array.isArray(p?.trrs)) return p.trrs
|
||||||
|
if (Array.isArray(p?.bank_accounts)) return p.bank_accounts
|
||||||
|
if (Array.isArray(p?.accounts)) return p.accounts
|
||||||
|
if (Array.isArray(p?.bankAccounts)) return p.bankAccounts
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -133,7 +229,10 @@ const operDrawerAddPhone = (edit = false, id = 0) => {
|
||||||
<FwbBadge type="yellow">{{ address.country }}</FwbBadge>
|
<FwbBadge type="yellow">{{ address.country }}</FwbBadge>
|
||||||
<FwbBadge>{{ address.type.name }}</FwbBadge>
|
<FwbBadge>{{ address.type.name }}</FwbBadge>
|
||||||
</div>
|
</div>
|
||||||
<button><EditIcon @click="openDrawerAddAddress(true, address.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
<div class="flex items-center gap-2">
|
||||||
|
<button><EditIcon @click="openDrawerAddAddress(true, address.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
<button @click="openConfirm('address', address.id, address.address)"><TrashBinIcon size="md" css="text-red-600 hover:text-red-700" /></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">{{ address.address }}</p>
|
<p class="text-sm md:text-base leading-7 text-gray-900">{{ address.address }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -152,12 +251,72 @@ const operDrawerAddPhone = (edit = false, id = 0) => {
|
||||||
<FwbBadge title type="yellow">+{{ phone.country_code }}</FwbBadge>
|
<FwbBadge title type="yellow">+{{ phone.country_code }}</FwbBadge>
|
||||||
<FwbBadge>{{ phone.type.name }}</FwbBadge>
|
<FwbBadge>{{ phone.type.name }}</FwbBadge>
|
||||||
</div>
|
</div>
|
||||||
<button><EditIcon @click="operDrawerAddPhone(true, phone.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
<div class="flex items-center gap-2">
|
||||||
|
<button><EditIcon @click="operDrawerAddPhone(true, phone.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
<button @click="openConfirm('phone', phone.id, phone.nu)"><TrashBinIcon size="md" css="text-red-600 hover:text-red-700" /></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">{{ phone.nu }}</p>
|
<p class="text-sm md:text-base leading-7 text-gray-900">{{ phone.nu }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CusTab>
|
</CusTab>
|
||||||
|
<CusTab name="emails" title="Email">
|
||||||
|
<div class="flex justify-end mb-2">
|
||||||
|
<span class="border-b-2 border-gray-500 hover:border-gray-800">
|
||||||
|
<button><PlusIcon @click="openDrawerAddEmail(false, 0)" size="lg" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
||||||
|
<template v-if="getEmails(person).length">
|
||||||
|
<div class="rounded p-2 shadow" v-for="(email, idx) in getEmails(person)" :key="idx">
|
||||||
|
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<FwbBadge v-if="email?.label">{{ email.label }}</FwbBadge>
|
||||||
|
<FwbBadge v-else type="indigo">Email</FwbBadge>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button><EditIcon @click="openDrawerAddEmail(true, email.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
<button @click="deleteEmail(email.id, email?.value || email?.email || email?.address)"><TrashBinIcon size="md" css="text-red-600 hover:text-red-700" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ email?.value || email?.email || email?.address || '-' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="email?.note" class="mt-1 text-xs text-gray-500 whitespace-pre-wrap">{{ email.note }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="p-2 text-sm text-gray-500">Ni e-poštnih naslovov.</p>
|
||||||
|
</div>
|
||||||
|
</CusTab>
|
||||||
|
<CusTab name="trr" title="TRR">
|
||||||
|
<div class="flex justify-end mb-2">
|
||||||
|
<span class="border-b-2 border-gray-500 hover:border-gray-800">
|
||||||
|
<button><PlusIcon @click="openDrawerAddTrr(false, 0)" size="lg" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
||||||
|
<template v-if="getTRRs(person).length">
|
||||||
|
<div class="rounded p-2 shadow" v-for="(acc, idx) in getTRRs(person)" :key="idx">
|
||||||
|
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<FwbBadge v-if="acc?.bank_name">{{ acc.bank_name }}</FwbBadge>
|
||||||
|
<FwbBadge v-if="acc?.holder_name" type="indigo">{{ acc.holder_name }}</FwbBadge>
|
||||||
|
<FwbBadge v-if="acc?.currency" type="yellow">{{ acc.currency }}</FwbBadge>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button><EditIcon @click="openDrawerAddTrr(true, acc.id)" size="md" css="text-gray-500 hover:text-gray-800" /></button>
|
||||||
|
<button @click="deleteTrr(acc.id, acc?.iban || acc?.account_number)"><TrashBinIcon size="md" css="text-red-600 hover:text-red-700" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ acc?.iban || acc?.account_number || acc?.account || acc?.nu || acc?.number || '-' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="acc?.notes" class="mt-1 text-xs text-gray-500 whitespace-pre-wrap">{{ acc.notes }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="p-2 text-sm text-gray-500">Ni TRR računov.</p>
|
||||||
|
</div>
|
||||||
|
</CusTab>
|
||||||
<CusTab name="other" title="Drugo">
|
<CusTab name="other" title="Drugo">
|
||||||
ssss4
|
ssss4
|
||||||
</CusTab>
|
</CusTab>
|
||||||
|
|
@ -185,5 +344,50 @@ const operDrawerAddPhone = (edit = false, id = 0) => {
|
||||||
:id="editPhoneId"
|
:id="editPhoneId"
|
||||||
:edit="editPhone"
|
:edit="editPhone"
|
||||||
/>
|
/>
|
||||||
|
<!-- Email dialogs -->
|
||||||
|
<EmailCreateForm
|
||||||
|
:show="drawerAddEmail && !editEmail"
|
||||||
|
@close="drawerAddEmail = false"
|
||||||
|
:person="person"
|
||||||
|
:types="types.email_types ?? []"
|
||||||
|
/>
|
||||||
|
<EmailUpdateForm
|
||||||
|
:show="drawerAddEmail && editEmail"
|
||||||
|
@close="drawerAddEmail = false"
|
||||||
|
:person="person"
|
||||||
|
:types="types.email_types ?? []"
|
||||||
|
:id="editEmailId"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- TRR dialogs -->
|
||||||
|
<TrrCreateForm
|
||||||
|
:show="drawerAddTrr && !editTrr"
|
||||||
|
@close="drawerAddTrr = false"
|
||||||
|
:person="person"
|
||||||
|
:types="types.trr_types ?? []"
|
||||||
|
:banks="types.banks ?? []"
|
||||||
|
:currencies="types.currencies ?? ['EUR']"
|
||||||
|
/>
|
||||||
|
<TrrUpdateForm
|
||||||
|
:show="drawerAddTrr && editTrr"
|
||||||
|
@close="drawerAddTrr = false"
|
||||||
|
:person="person"
|
||||||
|
:types="types.trr_types ?? []"
|
||||||
|
:banks="types.banks ?? []"
|
||||||
|
:currencies="types.currencies ?? ['EUR']"
|
||||||
|
:id="editTrrId"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Confirm deletion dialog -->
|
||||||
|
<ConfirmDialog
|
||||||
|
:show="confirm.show"
|
||||||
|
:title="confirm.title"
|
||||||
|
:message="confirm.message"
|
||||||
|
confirm-text="Izbriši"
|
||||||
|
cancel-text="Prekliči"
|
||||||
|
:danger="true"
|
||||||
|
@close="closeConfirm"
|
||||||
|
@confirm="onConfirmDelete"
|
||||||
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
177
resources/js/Components/TrrCreateForm.vue
Normal file
177
resources/js/Components/TrrCreateForm.vue
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import DialogModal from './DialogModal.vue';
|
||||||
|
import InputLabel from './InputLabel.vue';
|
||||||
|
import SectionTitle from './SectionTitle.vue';
|
||||||
|
import TextInput from './TextInput.vue';
|
||||||
|
import InputError from './InputError.vue';
|
||||||
|
import PrimaryButton from './PrimaryButton.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/*
|
||||||
|
TRR (bank account) create/update
|
||||||
|
Fields aligned to migration/model: iban, bank_name, bic_swift, account_number, routing_number, currency, country_code, holder_name, notes
|
||||||
|
Routes: person.trr.create / person.trr.update
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
person: { type: Object, required: true },
|
||||||
|
currencies: { type: Array, default: () => ['EUR'] },
|
||||||
|
edit: { type: Boolean, default: false },
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const processing = ref(false);
|
||||||
|
const errors = ref({});
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const close = () => { emit('close'); setTimeout(() => { errors.value = {}; }, 300); };
|
||||||
|
|
||||||
|
const initialCurrency = () => (props.currencies && props.currencies.length ? props.currencies[0] : 'EUR');
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
iban: '',
|
||||||
|
bank_name: '',
|
||||||
|
bic_swift: '',
|
||||||
|
account_number: '',
|
||||||
|
routing_number: '',
|
||||||
|
currency: initialCurrency(),
|
||||||
|
country_code: '',
|
||||||
|
holder_name: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = { iban: '', bank_name: '', bic_swift: '', account_number: '', routing_number: '', currency: initialCurrency(), country_code: '', holder_name: '', notes: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
processing.value = true; errors.value = {};
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(route('person.trr.create', props.person), form.value);
|
||||||
|
if (!Array.isArray(props.person.trrs)) props.person.trrs = (props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || []);
|
||||||
|
(props.person.trrs).push(data.trr);
|
||||||
|
processing.value = false; close(); resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = e?.response?.data?.errors || {}; processing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
processing.value = true; errors.value = {};
|
||||||
|
try {
|
||||||
|
const { data } = await axios.put(route('person.trr.update', { person: props.person, trr_id: props.id }), form.value);
|
||||||
|
let list = props.person.trrs || props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || [];
|
||||||
|
const idx = list.findIndex(a => a.id === data.trr.id);
|
||||||
|
if (idx !== -1) list[idx] = data.trr;
|
||||||
|
processing.value = false; close(); resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = e?.response?.data?.errors || {}; processing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
(id) => {
|
||||||
|
if (props.edit && id) {
|
||||||
|
const list = props.person.trrs || props.person.bank_accounts || props.person.accounts || props.person.bankAccounts || [];
|
||||||
|
const current = list.find(a => a.id === id);
|
||||||
|
if (current) {
|
||||||
|
form.value = {
|
||||||
|
iban: current.iban || current.account_number || current.number || '',
|
||||||
|
bank_name: current.bank_name || '',
|
||||||
|
bic_swift: current.bic_swift || '',
|
||||||
|
account_number: current.account_number || '',
|
||||||
|
routing_number: current.routing_number || '',
|
||||||
|
currency: current.currency || initialCurrency(),
|
||||||
|
country_code: current.country_code || '',
|
||||||
|
holder_name: current.holder_name || '',
|
||||||
|
notes: current.notes || ''
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const submit = () => (props.edit ? update() : create());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="close">
|
||||||
|
<template #title>
|
||||||
|
<span v-if="edit">Spremeni TRR</span>
|
||||||
|
<span v-else>Dodaj TRR</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<SectionTitle class="border-b mb-4">
|
||||||
|
<template #title>TRR</template>
|
||||||
|
</SectionTitle>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_iban" value="IBAN" />
|
||||||
|
<TextInput id="trr_iban" v-model="form.iban" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.iban" v-for="err in errors.iban" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_bank_name" value="Banka" />
|
||||||
|
<TextInput id="trr_bank_name" v-model="form.bank_name" type="text" class="mt-1 block w-full" autocomplete="organization" />
|
||||||
|
<InputError v-if="errors.bank_name" v-for="err in errors.bank_name" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_bic" value="BIC / SWIFT" />
|
||||||
|
<TextInput id="trr_bic" v-model="form.bic_swift" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.bic_swift" v-for="err in errors.bic_swift" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_accnum" value="Številka računa" />
|
||||||
|
<TextInput id="trr_accnum" v-model="form.account_number" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.account_number" v-for="err in errors.account_number" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_route" value="Usmerjevalna številka (routing)" />
|
||||||
|
<TextInput id="trr_route" v-model="form.routing_number" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.routing_number" v-for="err in errors.routing_number" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4" v-if="currencies && currencies.length">
|
||||||
|
<InputLabel for="trr_currency" value="Valuta" />
|
||||||
|
<select id="trr_currency" v-model="form.currency" class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm">
|
||||||
|
<option v-for="c in currencies" :key="c">{{ c }}</option>
|
||||||
|
</select>
|
||||||
|
<InputError v-if="errors.currency" v-for="err in errors.currency" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_cc" value="Koda države (2-znaki, npr. SI)" />
|
||||||
|
<TextInput id="trr_cc" v-model="form.country_code" type="text" class="mt-1 block w-full" autocomplete="country" />
|
||||||
|
<InputError v-if="errors.country_code" v-for="err in errors.country_code" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_holder" value="Imetnik računa" />
|
||||||
|
<TextInput id="trr_holder" v-model="form.holder_name" type="text" class="mt-1 block w-full" autocomplete="name" />
|
||||||
|
<InputError v-if="errors.holder_name" v-for="err in errors.holder_name" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<InputLabel for="trr_notes" value="Opombe" />
|
||||||
|
<TextInput id="trr_notes" v-model="form.notes" type="text" class="mt-1 block w-full" autocomplete="off" />
|
||||||
|
<InputError v-if="errors.notes" v-for="err in errors.notes" :key="err" :message="err" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<PrimaryButton :class="{ 'opacity-25': processing }" :disabled="processing">Shrani</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
17
resources/js/Components/TrrUpdateForm.vue
Normal file
17
resources/js/Components/TrrUpdateForm.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup>
|
||||||
|
// Thin wrapper to reuse TrrCreateForm with edit=true
|
||||||
|
import TrrCreateForm from './TrrCreateForm.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
person: { type: Object, required: true },
|
||||||
|
types: { type: Array, default: () => [] },
|
||||||
|
banks: { type: Array, default: () => [] },
|
||||||
|
currencies: { type: Array, default: () => ['EUR'] },
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TrrCreateForm :show="show" :person="person" :types="types" :banks="banks" :currencies="currencies" :edit="true" :id="id" @close="$emit('close')" />
|
||||||
|
</template>
|
||||||
|
|
@ -122,8 +122,10 @@ watch(
|
||||||
<aside :class="[
|
<aside :class="[
|
||||||
sidebarCollapsed ? 'w-16' : 'w-64',
|
sidebarCollapsed ? 'w-16' : 'w-64',
|
||||||
'bg-white border-r border-gray-200 transition-all duration-200 z-50',
|
'bg-white border-r border-gray-200 transition-all duration-200 z-50',
|
||||||
// Off-canvas behavior on mobile
|
// Off-canvas behavior on mobile; sticky fixed-like sidebar on desktop
|
||||||
isMobile ? 'fixed inset-y-0 left-0 transform ' + (mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full') : 'relative translate-x-0'
|
isMobile
|
||||||
|
? ('fixed inset-y-0 left-0 transform ' + (mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'))
|
||||||
|
: 'sticky top-0 h-screen overflow-y-auto'
|
||||||
]">
|
]">
|
||||||
<div class="h-16 px-4 flex items-center justify-between border-b">
|
<div class="h-16 px-4 flex items-center justify-between border-b">
|
||||||
<Link :href="route('dashboard')" class="flex items-center gap-2">
|
<Link :href="route('dashboard')" class="flex items-center gap-2">
|
||||||
|
|
@ -197,7 +199,7 @@ watch(
|
||||||
<!-- Main column -->
|
<!-- Main column -->
|
||||||
<div class="flex-1 flex flex-col min-w-0">
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
<div class="h-16 bg-white border-b border-gray-100 px-4 flex items-center justify-between">
|
<div class="h-16 bg-white border-b border-gray-100 px-4 flex items-center justify-between sticky top-0 z-30">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Sidebar toggle -->
|
<!-- Sidebar toggle -->
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import ActionMessage from '@/Components/ActionMessage.vue';
|
||||||
import BasicButton from '@/Components/buttons/BasicButton.vue';
|
import BasicButton from '@/Components/buttons/BasicButton.vue';
|
||||||
import DialogModal from '@/Components/DialogModal.vue';
|
import DialogModal from '@/Components/DialogModal.vue';
|
||||||
import InputLabel from '@/Components/InputLabel.vue';
|
import InputLabel from '@/Components/InputLabel.vue';
|
||||||
|
import DatePickerField from '@/Components/DatePickerField.vue';
|
||||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||||
import SectionTitle from '@/Components/SectionTitle.vue';
|
import SectionTitle from '@/Components/SectionTitle.vue';
|
||||||
import TextInput from '@/Components/TextInput.vue';
|
import TextInput from '@/Components/TextInput.vue';
|
||||||
|
|
@ -18,7 +19,9 @@ const props = defineProps({
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
client_case: Object,
|
client_case: Object,
|
||||||
actions: Array
|
actions: Array,
|
||||||
|
// optionally pre-select a contract to attach the activity to
|
||||||
|
contractUuid: { type: String, default: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
const decisions = ref(props.actions[0].decisions);
|
const decisions = ref(props.actions[0].decisions);
|
||||||
|
|
@ -36,7 +39,8 @@ const form = useForm({
|
||||||
amount: null,
|
amount: null,
|
||||||
note: '',
|
note: '',
|
||||||
action_id: props.actions[0].id,
|
action_id: props.actions[0].id,
|
||||||
decision_id: props.actions[0].decisions[0].id
|
decision_id: props.actions[0].decisions[0].id,
|
||||||
|
contract_uuid: props.contractUuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -52,14 +56,20 @@ watch(
|
||||||
(due_date) => {
|
(due_date) => {
|
||||||
if (due_date) {
|
if (due_date) {
|
||||||
let date = new Date(form.due_date).toISOString().split('T')[0];
|
let date = new Date(form.due_date).toISOString().split('T')[0];
|
||||||
console.table({old: due_date, new: date});
|
console.table({ old: due_date, new: date });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// keep contract_uuid synced if the prop changes while the drawer is open
|
||||||
|
watch(
|
||||||
|
() => props.contractUuid,
|
||||||
|
(cu) => { form.contract_uuid = cu || null; }
|
||||||
|
);
|
||||||
|
|
||||||
const store = () => {
|
const store = () => {
|
||||||
console.table({
|
console.table({
|
||||||
due_date: form.due_date,
|
due_date: form.due_date,
|
||||||
action_id: form.action_id,
|
action_id: form.action_id,
|
||||||
decision_id: form.decision_id,
|
decision_id: form.decision_id,
|
||||||
|
|
@ -68,10 +78,10 @@ const store = () => {
|
||||||
});
|
});
|
||||||
form.post(route('clientCase.activity.store', props.client_case), {
|
form.post(route('clientCase.activity.store', props.client_case), {
|
||||||
onBefore: () => {
|
onBefore: () => {
|
||||||
if(form.due_date) {
|
if (form.due_date) {
|
||||||
form.due_date = new Date(form.due_date).toISOString().split('T')[0];
|
form.due_date = new Date(form.due_date).toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
close();
|
close();
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
@ -87,75 +97,46 @@ const store = () => {
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<DialogModal
|
<DialogModal :show="show" @close="close">
|
||||||
:show="show"
|
|
||||||
@close="close"
|
|
||||||
>
|
|
||||||
<template #title>Dodaj aktivnost</template>
|
<template #title>Dodaj aktivnost</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<form @submit.prevent="store">
|
<form @submit.prevent="store">
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="activityAction" value="Akcija"/>
|
<InputLabel for="activityAction" value="Akcija" />
|
||||||
<select
|
<select
|
||||||
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
||||||
id="activityAction"
|
id="activityAction" ref="activityActionSelect" v-model="form.action_id">
|
||||||
ref="activityActionSelect"
|
|
||||||
v-model="form.action_id"
|
|
||||||
>
|
|
||||||
<option v-for="a in actions" :value="a.id">{{ a.name }}</option>
|
<option v-for="a in actions" :value="a.id">{{ a.name }}</option>
|
||||||
<!-- ... -->
|
<!-- ... -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="activityDecision" value="Odločitev"/>
|
<InputLabel for="activityDecision" value="Odločitev" />
|
||||||
<select
|
<select
|
||||||
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
||||||
id="activityDecision"
|
id="activityDecision" ref="activityDecisionSelect" v-model="form.decision_id">
|
||||||
ref="activityDecisionSelect"
|
|
||||||
v-model="form.decision_id"
|
|
||||||
>
|
|
||||||
<option v-for="d in decisions" :value="d.id">{{ d.name }}</option>
|
<option v-for="d in decisions" :value="d.id">{{ d.name }}</option>
|
||||||
<!-- ... -->
|
<!-- ... -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<FwbTextarea
|
<FwbTextarea label="Opomba" id="activityNote" ref="activityNoteTextarea" v-model="form.note"
|
||||||
label="Opomba"
|
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" />
|
||||||
id="activityNote"
|
|
||||||
ref="activityNoteTextarea"
|
|
||||||
v-model="form.note"
|
|
||||||
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DatePickerField id="activityDueDate" label="Datum zapadlosti" v-model="form.due_date"
|
||||||
|
format="dd.MM.yyyy" :enable-time-picker="false" :auto-position="true" :teleport-target="'body'"
|
||||||
|
:inline="false" :auto-apply="false" :fixed="false" :close-on-auto-apply="true"
|
||||||
|
:close-on-scroll="true" />
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="activityDueDate" value="Datum zapadlosti"/>
|
<InputLabel for="activityAmount" value="Znesek" />
|
||||||
<vue-date-picker
|
<TextInput id="activityAmount" ref="activityAmountinput" v-model="form.amount" type="number"
|
||||||
id="activityDueDate"
|
class="mt-1 block w-full" autocomplete="0.00" />
|
||||||
:enable-time-picker="false"
|
|
||||||
format="dd.MM.yyyy"
|
|
||||||
class="mt-1 block w-full"
|
|
||||||
v-model="form.due_date"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-6 sm:col-span-4">
|
|
||||||
<InputLabel for="activityAmount" value="Znesek"/>
|
|
||||||
<TextInput
|
|
||||||
id="activityAmount"
|
|
||||||
ref="activityAmountinput"
|
|
||||||
v-model="form.amount"
|
|
||||||
type="number"
|
|
||||||
class="mt-1 block w-full"
|
|
||||||
autocomplete="0.00"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<ActionMessage :on="form.recentlySuccessful" class="me-3">
|
<ActionMessage :on="form.recentlySuccessful" class="me-3">
|
||||||
Shranjuje.
|
Shranjuje.
|
||||||
</ActionMessage>
|
</ActionMessage>
|
||||||
<BasicButton
|
<BasicButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||||
:class="{ 'opacity-25': form.processing }"
|
|
||||||
:disabled="form.processing"
|
|
||||||
>
|
|
||||||
Shrani
|
Shrani
|
||||||
</BasicButton>
|
</BasicButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const props = defineProps({
|
||||||
|
|
||||||
|
|
||||||
let header = [
|
let header = [
|
||||||
|
C_TD.make('Pogodba', 'header'),
|
||||||
C_TD.make('Datum', 'header'),
|
C_TD.make('Datum', 'header'),
|
||||||
C_TD.make('Akcija', 'header'),
|
C_TD.make('Akcija', 'header'),
|
||||||
C_TD.make('Odločitev', 'header'),
|
C_TD.make('Odločitev', 'header'),
|
||||||
|
|
@ -26,6 +27,7 @@ const createBody = (data) => {
|
||||||
const dueDate = (p.due_date) ? new Date().toLocaleDateString('de') : null;
|
const dueDate = (p.due_date) ? new Date().toLocaleDateString('de') : null;
|
||||||
|
|
||||||
const cols = [
|
const cols = [
|
||||||
|
C_TD.make(p.contract?.reference ?? ''),
|
||||||
C_TD.make(createdDate, 'body' ),
|
C_TD.make(createdDate, 'body' ),
|
||||||
C_TD.make(p.action.name, 'body'),
|
C_TD.make(p.action.name, 'body'),
|
||||||
C_TD.make(p.decision.name, 'body'),
|
C_TD.make(p.decision.name, 'body'),
|
||||||
|
|
|
||||||
65
resources/js/Pages/Cases/Partials/CaseObjectCreateDialog.vue
Normal file
65
resources/js/Pages/Cases/Partials/CaseObjectCreateDialog.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup>
|
||||||
|
import DialogModal from '@/Components/DialogModal.vue'
|
||||||
|
import InputLabel from '@/Components/InputLabel.vue'
|
||||||
|
import TextInput from '@/Components/TextInput.vue'
|
||||||
|
import PrimaryButton from '@/Components/PrimaryButton.vue'
|
||||||
|
import SectionTitle from '@/Components/SectionTitle.vue'
|
||||||
|
import { useForm } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
client_case: { type: Object, required: true },
|
||||||
|
contract: { type: Object, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'created'])
|
||||||
|
const close = () => emit('close')
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
reference: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
type: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
form.post(route('clientCase.contract.object.store', { client_case: props.client_case.uuid, uuid: props.contract.uuid }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => { emit('created'); form.reset(); close() },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="close">
|
||||||
|
<template #title>Dodaj premet</template>
|
||||||
|
<template #content>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<SectionTitle class="mt-2 border-b mb-4">
|
||||||
|
<template #title>Premet</template>
|
||||||
|
</SectionTitle>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<InputLabel for="objRef" value="Referenca" />
|
||||||
|
<TextInput id="objRef" v-model="form.reference" type="text" class="mt-1 block w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<InputLabel for="objType" value="Tip" />
|
||||||
|
<TextInput id="objType" v-model="form.type" type="text" class="mt-1 block w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<InputLabel for="objName" value="Naziv" />
|
||||||
|
<TextInput id="objName" v-model="form.name" type="text" class="mt-1 block w-full" required />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<InputLabel for="objDesc" value="Opis" />
|
||||||
|
<textarea id="objDesc" v-model="form.description" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" rows="3" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">Shrani</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
52
resources/js/Pages/Cases/Partials/CaseObjectsDialog.vue
Normal file
52
resources/js/Pages/Cases/Partials/CaseObjectsDialog.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script setup>
|
||||||
|
import DialogModal from '@/Components/DialogModal.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
client_case: { type: Object, required: true },
|
||||||
|
contract: { type: Object, default: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
const close = () => emit('close')
|
||||||
|
|
||||||
|
const items = () => Array.isArray(props.contract?.objects) ? props.contract.objects : []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" @close="close">
|
||||||
|
<template #title>
|
||||||
|
Premeti
|
||||||
|
<span v-if="contract" class="ml-2 text-sm text-gray-500">(Pogodba: {{ contract.reference }})</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="mt-1 max-h-[60vh] overflow-y-auto">
|
||||||
|
<div v-if="items().length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="o in items()" :key="o.id" class="rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase text-gray-500">Ref.</div>
|
||||||
|
<div class="font-semibold text-gray-900">{{ o.reference || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="ml-3 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">{{ o.type || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-xs uppercase text-gray-500">Naziv</div>
|
||||||
|
<div class="text-gray-900">{{ o.name || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-xs uppercase text-gray-500">Opis</div>
|
||||||
|
<div class="text-gray-700 whitespace-pre-wrap">{{ o.description || '' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center text-gray-500 py-3">Ni predmetov.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<button type="button" class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50" @click="close">Zapri</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
|
|
@ -5,15 +5,16 @@ import InputLabel from '@/Components/InputLabel.vue';
|
||||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||||
import SectionTitle from '@/Components/SectionTitle.vue';
|
import SectionTitle from '@/Components/SectionTitle.vue';
|
||||||
import TextInput from '@/Components/TextInput.vue';
|
import TextInput from '@/Components/TextInput.vue';
|
||||||
|
import DatePickerField from '@/Components/DatePickerField.vue';
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client_case: Object,
|
client_case: Object,
|
||||||
show: {
|
show: { type: Boolean, default: false },
|
||||||
type: Boolean,
|
types: Array,
|
||||||
default: false
|
// Optional: when provided, drawer acts as edit mode
|
||||||
},
|
contract: { type: Object, default: null },
|
||||||
types: Array
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(props.types);
|
console.log(props.types);
|
||||||
|
|
@ -24,25 +25,59 @@ const close = () => {
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
//store contract
|
// form state for create or edit
|
||||||
const formContract = useForm({
|
const formContract = useForm({
|
||||||
client_case_uuid: props.client_case.uuid,
|
client_case_uuid: props.client_case.uuid,
|
||||||
reference: '',
|
uuid: props.contract?.uuid ?? null,
|
||||||
start_date: new Date().toISOString(),
|
reference: props.contract?.reference ?? '',
|
||||||
type_id: props.types[0].id
|
start_date: props.contract?.start_date ?? new Date().toISOString(),
|
||||||
|
type_id: (props.contract?.type_id ?? props.contract?.type?.id) ?? props.types[0].id,
|
||||||
|
description: props.contract?.description ?? '',
|
||||||
|
// nested account fields, if exists
|
||||||
|
initial_amount: props.contract?.account?.initial_amount ?? null,
|
||||||
|
balance_amount: props.contract?.account?.balance_amount ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const storeContract = () => {
|
// keep form in sync when switching between create and edit
|
||||||
formContract.post(route('clientCase.contract.store', props.client_case), {
|
const applyContract = (c) => {
|
||||||
|
formContract.uuid = c?.uuid ?? null
|
||||||
|
formContract.reference = c?.reference ?? ''
|
||||||
|
formContract.start_date = c?.start_date ?? new Date().toISOString()
|
||||||
|
formContract.type_id = (c?.type_id ?? c?.type?.id) ?? props.types[0].id
|
||||||
|
formContract.description = c?.description ?? ''
|
||||||
|
formContract.initial_amount = c?.account?.initial_amount ?? null
|
||||||
|
formContract.balance_amount = c?.account?.balance_amount ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.contract, (c) => {
|
||||||
|
applyContract(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.show, (open) => {
|
||||||
|
if (open && !props.contract) {
|
||||||
|
// reset for create
|
||||||
|
applyContract(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const storeOrUpdate = () => {
|
||||||
|
const isEdit = !!formContract.uuid
|
||||||
|
const options = {
|
||||||
onBefore: () => {
|
onBefore: () => {
|
||||||
formContract.start_date = formContract.start_date;
|
formContract.start_date = formContract.start_date
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
close();
|
close()
|
||||||
formContract.reset();
|
// keep state clean; reset to initial
|
||||||
|
if (!isEdit) formContract.reset()
|
||||||
},
|
},
|
||||||
preserveScroll: true
|
preserveScroll: true,
|
||||||
});
|
}
|
||||||
|
if (isEdit) {
|
||||||
|
formContract.put(route('clientCase.contract.update', { client_case: props.client_case.uuid, uuid: formContract.uuid }), options)
|
||||||
|
} else {
|
||||||
|
formContract.post(route('clientCase.contract.store', props.client_case), options)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -52,9 +87,9 @@ const storeContract = () => {
|
||||||
:show="show"
|
:show="show"
|
||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
<template #title>Dodaj pogodbo</template>
|
<template #title>{{ formContract.uuid ? 'Uredi pogodbo' : 'Dodaj pogodbo' }}</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<form @submit.prevent="storeContract">
|
<form @submit.prevent="storeOrUpdate">
|
||||||
<SectionTitle class="mt-4 border-b mb-4">
|
<SectionTitle class="mt-4 border-b mb-4">
|
||||||
<template #title>
|
<template #title>
|
||||||
Pogodba
|
Pogodba
|
||||||
|
|
@ -71,10 +106,13 @@ const storeContract = () => {
|
||||||
autocomplete="contract-reference"
|
autocomplete="contract-reference"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<DatePickerField
|
||||||
<InputLabel for="contractStartDate" value="Datum pričetka"/>
|
id="contractStartDate"
|
||||||
<vue-date-picker id="contractStartDate" :enable-time-picker="false" format="dd.MM.yyyy" class="mt-1 block w-full" v-model="formContract.start_date"></vue-date-picker>
|
label="Datum pričetka"
|
||||||
</div>
|
v-model="formContract.start_date"
|
||||||
|
format="dd.MM.yyyy"
|
||||||
|
:enable-time-picker="false"
|
||||||
|
/>
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="contractTypeSelect" value="Tip"/>
|
<InputLabel for="contractTypeSelect" value="Tip"/>
|
||||||
<select
|
<select
|
||||||
|
|
@ -86,13 +124,51 @@ const storeContract = () => {
|
||||||
<!-- ... -->
|
<!-- ... -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-span-6 sm:col-span-4 mt-4">
|
||||||
|
<InputLabel for="contractDescription" value="Opis"/>
|
||||||
|
<textarea
|
||||||
|
id="contractDescription"
|
||||||
|
v-model="formContract.description"
|
||||||
|
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SectionTitle class="mt-6 border-b mb-4">
|
||||||
|
<template #title>
|
||||||
|
Račun
|
||||||
|
</template>
|
||||||
|
</SectionTitle>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<InputLabel for="initialAmount" value="Predani znesek"/>
|
||||||
|
<TextInput
|
||||||
|
id="initialAmount"
|
||||||
|
v-model.number="formContract.initial_amount"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
class="mt-1 block w-full"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<InputLabel for="balanceAmount" value="Odprti znesek"/>
|
||||||
|
<TextInput
|
||||||
|
id="balanceAmount"
|
||||||
|
v-model.number="formContract.balance_amount"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
class="mt-1 block w-full"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<ActionMessage :on="formContract.recentlySuccessful" class="me-3">
|
<ActionMessage :on="formContract.recentlySuccessful" class="me-3">
|
||||||
Shranjuje.
|
Shranjuje.
|
||||||
</ActionMessage>
|
</ActionMessage>
|
||||||
|
|
||||||
<PrimaryButton :class="{ 'opacity-25': formContract.processing }" :disabled="formContract.processing">
|
<PrimaryButton :class="{ 'opacity-25': formContract.processing }" :disabled="formContract.processing">
|
||||||
Shrani
|
{{ formContract.uuid ? 'Posodobi' : 'Shrani' }}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,164 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import BasicTable from '@/Components/BasicTable.vue';
|
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell } from 'flowbite-vue'
|
||||||
import { LinkOptions as C_LINK, TableColumn as C_TD, TableRow as C_TR} from '@/Shared/AppObjects';
|
import Dropdown from '@/Components/Dropdown.vue'
|
||||||
|
import CaseObjectCreateDialog from './CaseObjectCreateDialog.vue'
|
||||||
|
import CaseObjectsDialog from './CaseObjectsDialog.vue'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { faCircleInfo, faEllipsisVertical, faPenToSquare, faTrash, faListCheck, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client_case: Object,
|
client_case: Object,
|
||||||
contract_types: Array,
|
contract_types: Array,
|
||||||
contracts: Array
|
contracts: { type: Array, default: () => [] },
|
||||||
});
|
})
|
||||||
|
|
||||||
//Contract table
|
const emit = defineEmits(['edit', 'delete', 'add-activity'])
|
||||||
let tableContractHeader = [
|
|
||||||
C_TD.make('Ref.', 'header'),
|
|
||||||
C_TD.make('Datum začetka', 'header'),
|
|
||||||
C_TD.make('Tip', 'header')
|
|
||||||
];
|
|
||||||
|
|
||||||
const tableOptions = {
|
const formatDate = (d) => {
|
||||||
editor_data: {
|
if (!d) return '-'
|
||||||
form: {
|
const dt = new Date(d)
|
||||||
route: {name: 'clientCase.contract.update', params: {uuid: props.client_case.uuid}},
|
return isNaN(dt.getTime()) ? '-' : dt.toLocaleDateString('de')
|
||||||
route_remove: {name: 'clientCase.contract.delete', params: {uuid: props.client_case.uuid}},
|
|
||||||
values: {
|
|
||||||
uuid: null,
|
|
||||||
reference: '',
|
|
||||||
type_id: 1
|
|
||||||
},
|
|
||||||
key: 'uuid',
|
|
||||||
index: {uuid: null},
|
|
||||||
el: [
|
|
||||||
{
|
|
||||||
id: 'contractRefU',
|
|
||||||
ref: 'contractRefUInput',
|
|
||||||
bind: 'reference',
|
|
||||||
type: 'text',
|
|
||||||
label: 'Referenca',
|
|
||||||
autocomplete: 'contract-reference'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'contractTypeU',
|
|
||||||
ref: 'contractTypeSelectU',
|
|
||||||
bind: 'type_id',
|
|
||||||
type: 'select',
|
|
||||||
label: 'Tip',
|
|
||||||
selectOptions: props.contract_types.map(item => new Object({val: item.id, desc: item.name}))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
title: 'contract'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createContractTableBody = (data) => {
|
const hasDesc = (c) => {
|
||||||
let tableContractBody = [];
|
const d = c?.description
|
||||||
|
return typeof d === 'string' && d.trim().length > 0
|
||||||
data.forEach((p) => {
|
|
||||||
let startDate = new Date(p.start_date).toLocaleDateString('de');
|
|
||||||
const cols = [
|
|
||||||
C_TD.make(p.reference, 'body' ),
|
|
||||||
C_TD.make(startDate, 'body' ),
|
|
||||||
C_TD.make(p.type.name, 'body' ),
|
|
||||||
];
|
|
||||||
|
|
||||||
tableContractBody.push(
|
|
||||||
C_TR.make(
|
|
||||||
cols,
|
|
||||||
{
|
|
||||||
class: '',
|
|
||||||
title: p.reference,
|
|
||||||
edit: true,
|
|
||||||
ref: {key: 'uuid', val: p.uuid},
|
|
||||||
editable: {
|
|
||||||
reference: p.reference,
|
|
||||||
type_id: p.type.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
return tableContractBody;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onEdit = (c) => emit('edit', c)
|
||||||
|
const onDelete = (c) => emit('delete', c)
|
||||||
|
const onAddActivity = (c) => emit('add-activity', c)
|
||||||
|
|
||||||
|
// CaseObject dialog state
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const showObjectDialog = ref(false)
|
||||||
|
const showObjectsList = ref(false)
|
||||||
|
const selectedContract = ref(null)
|
||||||
|
const openObjectDialog = (c) => { selectedContract.value = c; showObjectDialog.value = true }
|
||||||
|
const closeObjectDialog = () => { showObjectDialog.value = false; selectedContract.value = null }
|
||||||
|
const openObjectsList = (c) => { selectedContract.value = c; showObjectsList.value = true }
|
||||||
|
const closeObjectsList = () => { showObjectsList.value = false; selectedContract.value = null }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicTable :options="tableOptions" :header="tableContractHeader" :editor="true" :body="createContractTableBody(contracts)" />
|
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||||
|
<FwbTable hoverable striped class="text-sm">
|
||||||
|
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm">
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Ref.</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Datum začetka</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Tip</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-right">Predano</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-right">Odprto</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-center">Opis</FwbTableHeadCell>
|
||||||
|
<FwbTableHeadCell class="w-px" />
|
||||||
|
</FwbTableHead>
|
||||||
|
<FwbTableBody>
|
||||||
|
<template v-for="(c, i) in contracts" :key="c.uuid || i">
|
||||||
|
<FwbTableRow>
|
||||||
|
<FwbTableCell>{{ c.reference }}</FwbTableCell>
|
||||||
|
<FwbTableCell>{{ formatDate(c.start_date) }}</FwbTableCell>
|
||||||
|
<FwbTableCell>{{ c?.type?.name }}</FwbTableCell>
|
||||||
|
<FwbTableCell class="text-right">{{ Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(c?.account?.initial_amount ?? 0) }}</FwbTableCell>
|
||||||
|
<FwbTableCell class="text-right">{{ Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(c?.account?.balance_amount ?? 0) }}</FwbTableCell>
|
||||||
|
<FwbTableCell class="text-center">
|
||||||
|
<Dropdown v-if="hasDesc(c)" width="64" align="left">
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
:title="'Pokaži opis'"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="max-w-sm px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap">
|
||||||
|
{{ c.description }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
class="inline-flex items-center justify-center h-8 w-8 rounded-full text-gray-400 cursor-not-allowed"
|
||||||
|
:title="'Ni opisa'"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</FwbTableCell>
|
||||||
|
<FwbTableCell class="text-right whitespace-nowrap">
|
||||||
|
<Dropdown align="right" width="56">
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
:title="'Actions'"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<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="onEdit(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faPenToSquare" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</button>
|
||||||
|
<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="openObjectsList(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Predmeti</span>
|
||||||
|
</button>
|
||||||
|
<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="openObjectDialog(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Premet</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-left text-sm text-red-700 hover:bg-red-50 flex items-center gap-2"
|
||||||
|
@click="onDelete(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4 text-red-600" />
|
||||||
|
<span>Briši</span>
|
||||||
|
</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="onAddActivity(c)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faListCheck" class="h-4 w-4 text-gray-600" />
|
||||||
|
<span>Aktivnost</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</FwbTableCell>
|
||||||
|
</FwbTableRow>
|
||||||
|
</template>
|
||||||
|
</FwbTableBody>
|
||||||
|
</FwbTable>
|
||||||
|
<div v-if="!contracts || contracts.length === 0" class="p-6 text-center text-sm text-gray-500">No contracts.</div>
|
||||||
|
</div>
|
||||||
|
<CaseObjectCreateDialog
|
||||||
|
:show="showObjectDialog"
|
||||||
|
@close="closeObjectDialog"
|
||||||
|
:client_case="client_case"
|
||||||
|
:contract="selectedContract"
|
||||||
|
/>
|
||||||
|
<CaseObjectsDialog
|
||||||
|
:show="showObjectsList"
|
||||||
|
@close="closeObjectsList"
|
||||||
|
:client_case="client_case"
|
||||||
|
:contract="selectedContract"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -15,6 +15,7 @@ import { classifyDocument } from "@/Services/documents";
|
||||||
import { router } from '@inertiajs/vue3';
|
import { router } from '@inertiajs/vue3';
|
||||||
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";
|
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";
|
||||||
import Pagination from "@/Components/Pagination.vue";
|
import Pagination from "@/Components/Pagination.vue";
|
||||||
|
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client: Object,
|
client: Object,
|
||||||
|
|
@ -49,22 +50,42 @@ const openViewer = (doc) => {
|
||||||
};
|
};
|
||||||
const closeViewer = () => { viewer.value.open = false; viewer.value.src = ''; };
|
const closeViewer = () => { viewer.value.open = false; viewer.value.src = ''; };
|
||||||
|
|
||||||
const clientDetails = ref(true);
|
const clientDetails = ref(false);
|
||||||
|
|
||||||
//Drawer add new contract
|
// Contract drawer (create/edit)
|
||||||
const drawerCreateContract = ref(false);
|
const drawerCreateContract = ref(false);
|
||||||
|
const contractEditing = ref(null);
|
||||||
const openDrawerCreateContract = () => {
|
const openDrawerCreateContract = () => {
|
||||||
|
contractEditing.value = null;
|
||||||
|
drawerCreateContract.value = true;
|
||||||
|
};
|
||||||
|
const openDrawerEditContract = (c) => {
|
||||||
|
contractEditing.value = c;
|
||||||
drawerCreateContract.value = true;
|
drawerCreateContract.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
//Drawer add new activity
|
//Drawer add new activity
|
||||||
const drawerAddActivity = ref(false);
|
const drawerAddActivity = ref(false);
|
||||||
|
const activityContractUuid = ref(null);
|
||||||
|
|
||||||
const openDrawerAddActivity = () => {
|
const openDrawerAddActivity = (c = null) => {
|
||||||
|
activityContractUuid.value = c?.uuid ?? null;
|
||||||
drawerAddActivity.value = true;
|
drawerAddActivity.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// delete confirmation
|
||||||
|
const confirmDelete = ref({ show: false, contract: null })
|
||||||
|
const requestDeleteContract = (c) => { confirmDelete.value = { show: true, contract: c } }
|
||||||
|
const closeConfirmDelete = () => { confirmDelete.value.show = false; confirmDelete.value.contract = null }
|
||||||
|
const doDeleteContract = () => {
|
||||||
|
const c = confirmDelete.value.contract
|
||||||
|
if (!c) return closeConfirmDelete()
|
||||||
|
router.delete(route('clientCase.contract.delete', { client_case: props.client_case.uuid, uuid: c.uuid }), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onFinish: () => closeConfirmDelete(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//Close drawer (all)
|
//Close drawer (all)
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
drawerCreateContract.value = false;
|
drawerCreateContract.value = false;
|
||||||
|
|
@ -154,34 +175,15 @@ const hideClietnDetails = () => {
|
||||||
:client_case="client_case"
|
:client_case="client_case"
|
||||||
:contracts="contracts"
|
:contracts="contracts"
|
||||||
:contract_types="contract_types"
|
:contract_types="contract_types"
|
||||||
|
@edit="openDrawerEditContract"
|
||||||
|
@delete="requestDeleteContract"
|
||||||
|
@add-activity="openDrawerAddActivity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Documents section -->
|
|
||||||
<div class="pt-12">
|
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4">
|
|
||||||
<div class="mx-auto max-w-4x1">
|
|
||||||
<div class="flex justify-between p-4">
|
|
||||||
<SectionTitle>
|
|
||||||
<template #title>Dokumenti</template>
|
|
||||||
</SectionTitle>
|
|
||||||
<FwbButton @click="openUpload">Dodaj</FwbButton>
|
|
||||||
</div>
|
|
||||||
<DocumentsTable :documents="documents" @view="openViewer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DocumentUploadDialog
|
|
||||||
:show="showUpload"
|
|
||||||
@close="closeUpload"
|
|
||||||
@uploaded="onUploaded"
|
|
||||||
:post-url="route('clientCase.document.store', client_case)"
|
|
||||||
/>
|
|
||||||
<DocumentViewerDialog :show="viewer.open" :src="viewer.src" :title="viewer.title" @close="closeViewer" />
|
|
||||||
<div class="pt-12 pb-6">
|
<div class="pt-12 pb-6">
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4">
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4">
|
||||||
|
|
@ -199,17 +201,55 @@ const hideClietnDetails = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Documents section -->
|
||||||
|
<div class="pt-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4">
|
||||||
|
<div class="mx-auto max-w-4x1">
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<SectionTitle>
|
||||||
|
<template #title>Dokumenti</template>
|
||||||
|
</SectionTitle>
|
||||||
|
<FwbButton @click="openUpload">Dodaj</FwbButton>
|
||||||
|
</div>
|
||||||
|
<DocumentsTable
|
||||||
|
:documents="documents"
|
||||||
|
@view="openViewer"
|
||||||
|
:download-url-builder="doc => route('clientCase.document.download', { client_case: client_case.uuid, document: doc.uuid })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DocumentUploadDialog
|
||||||
|
:show="showUpload"
|
||||||
|
@close="closeUpload"
|
||||||
|
@uploaded="onUploaded"
|
||||||
|
:post-url="route('clientCase.document.store', client_case)"
|
||||||
|
/>
|
||||||
|
<DocumentViewerDialog :show="viewer.open" :src="viewer.src" :title="viewer.title" @close="closeViewer" />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
<ContractDrawer
|
<ContractDrawer
|
||||||
:show="drawerCreateContract"
|
:show="drawerCreateContract"
|
||||||
@close="closeDrawer"
|
@close="closeDrawer"
|
||||||
:types="contract_types"
|
:types="contract_types"
|
||||||
:client_case="client_case"
|
:client_case="client_case"
|
||||||
|
:contract="contractEditing"
|
||||||
/>
|
/>
|
||||||
<ActivityDrawer
|
<ActivityDrawer
|
||||||
:show="drawerAddActivity"
|
:show="drawerAddActivity"
|
||||||
@close="closeDrawer"
|
@close="closeDrawer"
|
||||||
:client_case="client_case"
|
:client_case="client_case"
|
||||||
:actions="actions"
|
:actions="actions"
|
||||||
|
:contract-uuid="activityContractUuid"
|
||||||
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
:show="confirmDelete.show"
|
||||||
|
title="Izbriši pogodbo"
|
||||||
|
message="Ali ste prepričani, da želite izbrisati pogodbo?"
|
||||||
|
confirm-text="Izbriši"
|
||||||
|
:danger="true"
|
||||||
|
@close="closeConfirmDelete"
|
||||||
|
@confirm="doDeleteContract"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ async function uploadAndPreview() {
|
||||||
processResult.value = null;
|
processResult.value = null;
|
||||||
const fd = new window.FormData();
|
const fd = new window.FormData();
|
||||||
fd.append('file', form.file);
|
fd.append('file', form.file);
|
||||||
if (Number.isFinite(form.import_template_id)) {
|
if (form.import_template_id !== null && form.import_template_id !== undefined && String(form.import_template_id).trim() !== '') {
|
||||||
fd.append('import_template_id', String(form.import_template_id));
|
fd.append('import_template_id', String(form.import_template_id));
|
||||||
}
|
}
|
||||||
if (form.client_uuid) {
|
if (form.client_uuid) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const props = defineProps({
|
||||||
import: Object,
|
import: Object,
|
||||||
templates: Array,
|
templates: Array,
|
||||||
clients: Array,
|
clients: Array,
|
||||||
|
client: Object
|
||||||
});
|
});
|
||||||
|
|
||||||
const importId = ref(props.import?.id || null);
|
const importId = ref(props.import?.id || null);
|
||||||
|
|
@ -227,13 +228,13 @@ const fieldOptionsByEntity = {
|
||||||
|
|
||||||
// Local state for selects
|
// Local state for selects
|
||||||
const form = ref({
|
const form = ref({
|
||||||
client_uuid: null,
|
client_uuid: props.client.uuid,
|
||||||
import_template_id: props.import?.import_template_id || null,
|
import_template_id: props.import?.import_template_id || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize client_uuid from numeric client_id using provided clients list
|
// Initialize client_uuid from import.client_uuid (preferred) using provided clients list
|
||||||
if (props.import?.client_id) {
|
if (props.import?.client_uuid) {
|
||||||
const found = (props.clients || []).find(c => c.id === props.import.client_id);
|
const found = (props.clients || []).find(c => c.uuid === props.import.client_uuid);
|
||||||
form.value.client_uuid = found ? found.uuid : null;
|
form.value.client_uuid = found ? found.uuid : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function statusBadge(status) {
|
||||||
<td class="p-2 whitespace-nowrap">{{ new Date(imp.created_at).toLocaleString() }}</td>
|
<td class="p-2 whitespace-nowrap">{{ new Date(imp.created_at).toLocaleString() }}</td>
|
||||||
<td class="p-2">{{ imp.original_name }}</td>
|
<td class="p-2">{{ imp.original_name }}</td>
|
||||||
<td class="p-2"><span :class="['px-2 py-0.5 rounded text-xs', statusBadge(imp.status)]">{{ imp.status }}</span></td>
|
<td class="p-2"><span :class="['px-2 py-0.5 rounded text-xs', statusBadge(imp.status)]">{{ imp.status }}</span></td>
|
||||||
<td class="p-2">{{ imp.client?.uuid ?? '—' }}</td>
|
<td class="p-2">{{ imp.client?.person?.full_name ?? '—' }}</td>
|
||||||
<td class="p-2">{{ imp.template?.name ?? '—' }}</td>
|
<td class="p-2">{{ imp.template?.name ?? '—' }}</td>
|
||||||
<td class="p-2 space-x-2">
|
<td class="p-2 space-x-2">
|
||||||
<Link :href="route('imports.continue', { import: imp.uuid })" class="px-2 py-1 rounded bg-gray-200 text-gray-800 text-xs">Poglej</Link>
|
<Link :href="route('imports.continue', { import: imp.uuid })" class="px-2 py-1 rounded bg-gray-200 text-gray-800 text-xs">Poglej</Link>
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ const store = () => {
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:taggable="false"
|
:taggable="false"
|
||||||
placeholder="Izberi segment"
|
placeholder="Izberi segment"
|
||||||
|
:append-to-body="true"
|
||||||
:custom-label="(opt) => (segmentOptions.find(s=>s.id===opt)?.name || '')"
|
:custom-label="(opt) => (segmentOptions.find(s=>s.id===opt)?.name || '')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -196,6 +197,7 @@ const store = () => {
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
placeholder="Dodaj odločitev"
|
placeholder="Dodaj odločitev"
|
||||||
|
:append-to-body="true"
|
||||||
label="name"
|
label="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -256,6 +258,7 @@ const store = () => {
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:taggable="false"
|
:taggable="false"
|
||||||
placeholder="Izberi segment"
|
placeholder="Izberi segment"
|
||||||
|
:append-to-body="true"
|
||||||
:custom-label="(opt) => (segmentOptions.find(s=>s.id===opt)?.name || '')"
|
:custom-label="(opt) => (segmentOptions.find(s=>s.id===opt)?.name || '')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,6 +273,7 @@ const store = () => {
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
placeholder="Dodaj odločitev"
|
placeholder="Dodaj odločitev"
|
||||||
|
:append-to-body="true"
|
||||||
label="name"
|
label="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ const store = () => {
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
placeholder="Dodaj akcijo"
|
placeholder="Dodaj akcijo"
|
||||||
|
:append-to-body="true"
|
||||||
label="name"
|
label="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -219,6 +220,7 @@ const store = () => {
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
placeholder="Dodaj akcijo"
|
placeholder="Dodaj akcijo"
|
||||||
|
:append-to-body="true"
|
||||||
label="name"
|
label="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import VueApexCharts from 'vue3-apexcharts';
|
||||||
import VueDatePicker from '@vuepic/vue-datepicker';
|
import VueDatePicker from '@vuepic/vue-datepicker';
|
||||||
import '@vuepic/vue-datepicker/dist/main.css';
|
import '@vuepic/vue-datepicker/dist/main.css';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
import VCalendar from 'v-calendar';
|
||||||
|
import 'v-calendar/style.css';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
|
|
@ -19,6 +21,8 @@ createInertiaApp({
|
||||||
return createApp({ render: () => h(App, props) })
|
return createApp({ render: () => h(App, props) })
|
||||||
.use(plugin)
|
.use(plugin)
|
||||||
.use(ZiggyVue)
|
.use(ZiggyVue)
|
||||||
|
// Register v-calendar with a 'V' prefix so we get <VDatePicker> and <VCalendar>
|
||||||
|
.use(VCalendar, { componentPrefix: 'V' })
|
||||||
.use(VueApexCharts)
|
.use(VueApexCharts)
|
||||||
.component('vue-date-picker', VueDatePicker)
|
.component('vue-date-picker', VueDatePicker)
|
||||||
.component('FontAwesomeIcon', FontAwesomeIcon)
|
.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
use App\Http\Controllers\SettingController;
|
use App\Http\Controllers\SettingController;
|
||||||
use App\Http\Controllers\ImportController;
|
use App\Http\Controllers\ImportController;
|
||||||
use App\Http\Controllers\ImportTemplateController;
|
use App\Http\Controllers\ImportTemplateController;
|
||||||
|
use App\Http\Controllers\CaseObjectController;
|
||||||
use App\Models\Person\Person;
|
use App\Models\Person\Person;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use ArielMejiaDev\LarapexCharts\LarapexChart;
|
use ArielMejiaDev\LarapexCharts\LarapexChart;
|
||||||
|
|
@ -94,10 +95,17 @@
|
||||||
Route::put('person/{person:uuid}', [PersonController::class, 'update'])->name('person.update');
|
Route::put('person/{person:uuid}', [PersonController::class, 'update'])->name('person.update');
|
||||||
Route::post('person/{person:uuid}/address', [PersonController::class, 'createAddress'])->name('person.address.create');
|
Route::post('person/{person:uuid}/address', [PersonController::class, 'createAddress'])->name('person.address.create');
|
||||||
Route::put('person/{person:uuid}/address/{address_id}', [PersonController::class, 'updateAddress'])->name('person.address.update');
|
Route::put('person/{person:uuid}/address/{address_id}', [PersonController::class, 'updateAddress'])->name('person.address.update');
|
||||||
|
Route::delete('person/{person:uuid}/address/{address_id}', [PersonController::class, 'deleteAddress'])->name('person.address.delete');
|
||||||
Route::post('person/{person:uuid}/phone', [PersonController::class, 'createPhone'])->name('person.phone.create');
|
Route::post('person/{person:uuid}/phone', [PersonController::class, 'createPhone'])->name('person.phone.create');
|
||||||
Route::put('person/{person:uuid}/phone/{phone_id}', [PersonController::class, 'updatePhone'])->name('person.phone.update');
|
Route::put('person/{person:uuid}/phone/{phone_id}', [PersonController::class, 'updatePhone'])->name('person.phone.update');
|
||||||
|
Route::delete('person/{person:uuid}/phone/{phone_id}', [PersonController::class, 'deletePhone'])->name('person.phone.delete');
|
||||||
Route::post('person/{person:uuid}/email', [PersonController::class, 'createEmail'])->name('person.email.create');
|
Route::post('person/{person:uuid}/email', [PersonController::class, 'createEmail'])->name('person.email.create');
|
||||||
Route::put('person/{person:uuid}/email/{email_id}', [PersonController::class, 'updateEmail'])->name('person.email.update');
|
Route::put('person/{person:uuid}/email/{email_id}', [PersonController::class, 'updateEmail'])->name('person.email.update');
|
||||||
|
Route::delete('person/{person:uuid}/email/{email_id}', [PersonController::class, 'deleteEmail'])->name('person.email.delete');
|
||||||
|
// TRR (Bank account) endpoints
|
||||||
|
Route::post('person/{person:uuid}/trr', [PersonController::class, 'createTrr'])->name('person.trr.create');
|
||||||
|
Route::put('person/{person:uuid}/trr/{trr_id}', [PersonController::class, 'updateTrr'])->name('person.trr.update');
|
||||||
|
Route::delete('person/{person:uuid}/trr/{trr_id}', [PersonController::class, 'deleteTrr'])->name('person.trr.delete');
|
||||||
//client
|
//client
|
||||||
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
||||||
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
||||||
|
|
@ -112,6 +120,10 @@
|
||||||
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
|
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
|
||||||
Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');
|
Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');
|
||||||
Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
|
Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
|
||||||
|
// client-case / contract / objects
|
||||||
|
Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store');
|
||||||
|
Route::put('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'update'])->name('clientCase.object.update');
|
||||||
|
Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete');
|
||||||
//client-case / activity
|
//client-case / activity
|
||||||
Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store');
|
Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store');
|
||||||
//client-case / documents
|
//client-case / documents
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user