From e0303ece74471851360a27b7c24025620b2cdf95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Sun, 12 Oct 2025 12:24:17 +0200 Subject: [PATCH] documents --- .../Admin/DocumentTemplateController.php | 6 + .../ContractDocumentGenerationController.php | 3 + app/Http/Controllers/PersonController.php | 51 +++++ .../UpdateDocumentTemplateRequest.php | 4 + app/Models/DocumentSetting.php | 3 + app/Models/Person/PersonAddress.php | 14 +- app/Services/Documents/DocumentSettings.php | 12 + .../Documents/DocxTemplateRenderer.php | 27 ++- app/Services/Documents/TokenScanner.php | 3 +- app/Services/Documents/TokenValueResolver.php | 131 ++++++++++- app/Services/ImportProcessor.php | 8 +- config/documents.php | 23 +- .../factories/Person/PersonAddressFactory.php | 2 + ...post_code_and_city_to_person_addresses.php | 28 +++ ...d_custom_defaults_to_document_settings.php | 26 +++ resources/js/Components/AddressCreateForm.vue | 113 ++++++---- resources/js/Components/AddressUpdateForm.vue | 206 ++++++++++++++++-- resources/js/Components/PersonInfoGrid.vue | 15 +- .../js/Pages/Admin/DocumentTemplates/Edit.vue | 79 ++++++- tests/Feature/DocumentCustomTokensTest.php | 62 ++++++ .../Feature/DocumentCustomTokensTypesTest.php | 90 ++++++++ tests/Feature/DocumentNestedEntitiesTest.php | 80 +++++++ 22 files changed, 898 insertions(+), 88 deletions(-) create mode 100644 database/migrations/2025_10_12_120500_add_post_code_and_city_to_person_addresses.php create mode 100644 database/migrations/2025_10_12_150000_add_custom_defaults_to_document_settings.php create mode 100644 tests/Feature/DocumentCustomTokensTest.php create mode 100644 tests/Feature/DocumentCustomTokensTypesTest.php create mode 100644 tests/Feature/DocumentNestedEntitiesTest.php diff --git a/app/Http/Controllers/Admin/DocumentTemplateController.php b/app/Http/Controllers/Admin/DocumentTemplateController.php index f1adfb7..1308bbc 100644 --- a/app/Http/Controllers/Admin/DocumentTemplateController.php +++ b/app/Http/Controllers/Admin/DocumentTemplateController.php @@ -46,6 +46,7 @@ public function toggleActive(DocumentTemplate $template) public function show(DocumentTemplate $template) { $this->ensurePermission(); + return Inertia::render('Admin/DocumentTemplates/Show', [ 'template' => $template, ]); @@ -121,6 +122,11 @@ public function updateSettings(UpdateDocumentTemplateRequest $request, DocumentT if ($dirty) { $template->formatting_options = $fmt; } + // Merge meta, including custom_defaults + if ($request->has('meta') && is_array($request->input('meta'))) { + $meta = array_filter($request->input('meta'), fn ($v) => $v !== null && $v !== ''); + $template->meta = array_replace($template->meta ?? [], $meta); + } $template->updated_by = Auth::id(); $template->save(); diff --git a/app/Http/Controllers/ContractDocumentGenerationController.php b/app/Http/Controllers/ContractDocumentGenerationController.php index e3b8090..fd8003a 100644 --- a/app/Http/Controllers/ContractDocumentGenerationController.php +++ b/app/Http/Controllers/ContractDocumentGenerationController.php @@ -23,6 +23,8 @@ public function __invoke(Request $request, Contract $contract): Response } $request->validate([ 'template_slug' => ['required', 'string', 'exists:document_templates,slug'], + 'custom' => ['nullable', 'array'], + 'custom.*' => ['nullable'], ]); $template = DocumentTemplate::where('slug', $request->template_slug) @@ -36,6 +38,7 @@ public function __invoke(Request $request, Contract $contract): Response $renderer = app(\App\Services\Documents\DocxTemplateRenderer::class); try { + // For custom tokens: pass overrides via request bag; service already reads request()->input('custom') if present. $result = $renderer->render($template, $contract, Auth::user()); } catch (\App\Services\Documents\Exceptions\UnresolvedTokensException $e) { return response()->json([ diff --git a/app/Http/Controllers/PersonController.php b/app/Http/Controllers/PersonController.php index 6dba1a2..59be3ca 100644 --- a/app/Http/Controllers/PersonController.php +++ b/app/Http/Controllers/PersonController.php @@ -26,6 +26,10 @@ public function update(Person $person, Request $request) $person->update($attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'Person updated'); + } + return response()->json([ 'person' => [ 'full_name' => $person->full_name, @@ -41,6 +45,8 @@ public function createAddress(Person $person, Request $request) $attributes = $request->validate([ 'address' => 'required|string|max:150', 'country' => 'nullable|string', + 'post_code' => 'nullable|string|max:16', + 'city' => 'nullable|string|max:100', 'type_id' => 'required|integer|exists:address_types,id', 'description' => 'nullable|string|max:125', ]); @@ -49,8 +55,15 @@ public function createAddress(Person $person, Request $request) $address = $person->addresses()->firstOrCreate([ 'address' => $attributes['address'], 'country' => $attributes['country'] ?? null, + 'post_code' => $attributes['post_code'] ?? null, + 'city' => $attributes['city'] ?? null, ], $attributes); + // Support Inertia form submissions (redirect back) and JSON (for API/axios) + if ($request->header('X-Inertia')) { + return back()->with('success', 'Address created'); + } + return response()->json([ 'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address->id), ]); @@ -61,6 +74,8 @@ public function updateAddress(Person $person, int $address_id, Request $request) $attributes = $request->validate([ 'address' => 'required|string|max:150', 'country' => 'nullable|string', + 'post_code' => 'nullable|string|max:16', + 'city' => 'nullable|string|max:100', 'type_id' => 'required|integer|exists:address_types,id', 'description' => 'nullable|string|max:125', ]); @@ -69,6 +84,10 @@ public function updateAddress(Person $person, int $address_id, Request $request) $address->update($attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'Address updated'); + } + return response()->json([ 'address' => $address, ]); @@ -79,6 +98,10 @@ public function deleteAddress(Person $person, int $address_id, Request $request) $address = $person->addresses()->findOrFail($address_id); $address->delete(); // soft delete + if ($request->header('X-Inertia')) { + return back()->with('success', 'Address deleted'); + } + return response()->json(['status' => 'ok']); } @@ -97,6 +120,10 @@ public function createPhone(Person $person, Request $request) 'country_code' => $attributes['country_code'] ?? null, ], $attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'Phone added successfully'); + } + return response()->json([ 'phone' => \App\Models\Person\PersonPhone::with(['type'])->findOrFail($phone->id), ]); @@ -115,6 +142,10 @@ public function updatePhone(Person $person, int $phone_id, Request $request) $phone->update($attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'Phone updated successfully'); + } + return response()->json([ 'phone' => $phone, ]); @@ -125,6 +156,10 @@ public function deletePhone(Person $person, int $phone_id, Request $request) $phone = $person->phones()->findOrFail($phone_id); $phone->delete(); // soft delete + if ($request->header('X-Inertia')) { + return back()->with('success', 'Phone deleted'); + } + return response()->json(['status' => 'ok']); } @@ -176,6 +211,10 @@ public function deleteEmail(Person $person, int $email_id, Request $request) $email = $person->emails()->findOrFail($email_id); $email->delete(); + if ($request->header('X-Inertia')) { + return back()->with('success', 'Email deleted'); + } + return response()->json(['status' => 'ok']); } @@ -198,6 +237,10 @@ public function createTrr(Person $person, Request $request) // Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided $trr = $person->bankAccounts()->create($attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'TRR added successfully'); + } + return response()->json([ 'trr' => BankAccount::findOrFail($trr->id), ]); @@ -222,6 +265,10 @@ public function updateTrr(Person $person, int $trr_id, Request $request) $trr = $person->bankAccounts()->findOrFail($trr_id); $trr->update($attributes); + if ($request->header('X-Inertia')) { + return back()->with('success', 'TRR updated successfully'); + } + return response()->json([ 'trr' => $trr, ]); @@ -232,6 +279,10 @@ public function deleteTrr(Person $person, int $trr_id, Request $request) $trr = $person->bankAccounts()->findOrFail($trr_id); $trr->delete(); + if ($request->header('X-Inertia')) { + return back()->with('success', 'TRR deleted'); + } + return response()->json(['status' => 'ok']); } } diff --git a/app/Http/Requests/UpdateDocumentTemplateRequest.php b/app/Http/Requests/UpdateDocumentTemplateRequest.php index b55ba9c..7352e49 100644 --- a/app/Http/Requests/UpdateDocumentTemplateRequest.php +++ b/app/Http/Requests/UpdateDocumentTemplateRequest.php @@ -30,6 +30,10 @@ public function rules(): array 'date_formats.*' => ['nullable', 'string', 'max:40'], 'meta' => ['sometimes', 'array'], 'meta.*' => ['nullable'], + 'meta.custom_defaults' => ['nullable', 'array'], + 'meta.custom_defaults.*' => ['nullable'], + 'meta.custom_default_types' => ['nullable', 'array'], + 'meta.custom_default_types.*' => ['nullable', 'in:string,number,date'], 'action_id' => ['nullable', 'integer', 'exists:actions,id'], 'decision_id' => ['nullable', 'integer', 'exists:decisions,id'], 'activity_note_template' => ['nullable', 'string'], diff --git a/app/Models/DocumentSetting.php b/app/Models/DocumentSetting.php index 727e8cb..e731278 100644 --- a/app/Models/DocumentSetting.php +++ b/app/Models/DocumentSetting.php @@ -13,12 +13,14 @@ class DocumentSetting extends Model 'preview_enabled', 'whitelist', 'date_formats', + 'custom_defaults', ]; protected $casts = [ 'preview_enabled' => 'boolean', 'whitelist' => 'array', 'date_formats' => 'array', + 'custom_defaults' => 'array', ]; public static function instance(): self @@ -30,6 +32,7 @@ public static function instance(): self 'preview_enabled' => config('documents.preview.enabled', true), 'whitelist' => config('documents.whitelist'), 'date_formats' => [], + 'custom_defaults' => [], ]); } } diff --git a/app/Models/Person/PersonAddress.php b/app/Models/Person/PersonAddress.php index 35f381f..455dbf9 100644 --- a/app/Models/Person/PersonAddress.php +++ b/app/Models/Person/PersonAddress.php @@ -12,33 +12,39 @@ class PersonAddress extends Model { /** @use HasFactory<\Database\Factories\Person/PersonAddressFactory> */ use HasFactory; + use Searchable; use SoftDeletes; protected $fillable = [ 'address', 'country', + 'post_code', + 'city', 'type_id', 'description', 'person_id', - 'user_id' + 'user_id', ]; protected $hidden = [ 'user_id', 'person_id', - 'deleted' + 'deleted', ]; public function toSearchableArray(): array { return [ 'address' => $this->address, - 'country' => $this->country + 'country' => $this->country, + 'post_code' => $this->post_code, + 'city' => $this->city, ]; } - protected static function booted(){ + protected static function booted() + { static::creating(function (PersonAddress $address) { $address->user_id = auth()->id(); }); diff --git a/app/Services/Documents/DocumentSettings.php b/app/Services/Documents/DocumentSettings.php index 2251ef0..e04da25 100644 --- a/app/Services/Documents/DocumentSettings.php +++ b/app/Services/Documents/DocumentSettings.php @@ -25,4 +25,16 @@ public function fresh(): DocumentSetting { return $this->refresh(); } + + /** + * Convenience accessor for custom defaults. + * + * @return array + */ + public function customDefaults(): array + { + $settings = $this->get(); + + return is_array($settings->custom_defaults ?? null) ? $settings->custom_defaults : []; + } } diff --git a/app/Services/Documents/DocxTemplateRenderer.php b/app/Services/Documents/DocxTemplateRenderer.php index 9ca68c8..c0ffdc5 100644 --- a/app/Services/Documents/DocxTemplateRenderer.php +++ b/app/Services/Documents/DocxTemplateRenderer.php @@ -41,9 +41,22 @@ public function render(DocumentTemplate $template, Contract $contract, User $use // Determine effective unresolved policy early (template override -> global -> config) $globalSettingsEarly = app(\App\Services\Documents\DocumentSettings::class)->get(); $effectivePolicy = $template->fail_on_unresolved ? 'fail' : ($globalSettingsEarly->unresolved_policy ?? config('documents.unresolved_policy', 'fail')); - $resolved = $this->resolver->resolve($tokens, $template, $contract, $user, $effectivePolicy); + // Resolve with support for custom.* tokens: per-generation overrides and defaults from template meta or global settings. + $customOverrides = request()->input('custom', []); // if called via HTTP context; otherwise pass explicitly from caller + $customDefaults = is_array($template->meta['custom_defaults'] ?? null) ? $template->meta['custom_defaults'] : null; + $resolved = $this->resolver->resolve( + $tokens, + $template, + $contract, + $user, + $effectivePolicy, + is_array($customOverrides) ? $customOverrides : [], + $customDefaults, + 'empty' + ); $values = $resolved['values']; $initialUnresolved = $resolved['unresolved']; + $customTypes = $resolved['customTypes'] ?? []; // Formatting options $fmt = $template->formatting_options ?? []; $decimals = (int) ($fmt['number_decimals'] ?? 2); @@ -55,8 +68,10 @@ public function render(DocumentTemplate $template, Contract $contract, User $use $globalSettings = app(\App\Services\Documents\DocumentSettings::class)->get(); $globalDateFormats = $globalSettings->date_formats ?? []; foreach ($values as $k => $v) { - // Date formatting (heuristic based on key ending with _date or .date) - if (is_string($v) && ($k === 'generation.date' || preg_match('/(^|\.)[A-Za-z_]*date$/i', $k))) { + $isTypedDate = ($customTypes[$k] ?? null) === 'date'; + $isTypedNumber = ($customTypes[$k] ?? null) === 'number'; + // Date formatting (typed or heuristic based on key ending with _date or .date) + if (is_string($v) && ($isTypedDate || $k === 'generation.date' || preg_match('/(^|\.)[A-Za-z_]*date$/i', $k))) { $dateFmtOverrides = $fmt['date_formats'] ?? []; $desiredFormat = $dateFmtOverrides[$k] ?? ($globalDateFormats[$k] ?? null) @@ -75,9 +90,11 @@ public function render(DocumentTemplate $template, Contract $contract, User $use } } } - if (is_numeric($v)) { + // Number formatting: only for explicitly typed numbers or common monetary fields + $isFinanceField = (bool) preg_match('/(^|\.)\b(amount|balance|total|price|cost)\b$/i', $k); + if (($isTypedNumber || $isFinanceField) && is_numeric($v)) { $num = number_format((float) $v, $decimals, $decSep, $thouSep); - if ($currencySymbol && preg_match('/(amount|balance|total|price|cost)/i', $k)) { + if ($currencySymbol && $isFinanceField) { $space = $currencySpace ? ' ' : ''; if ($currencyPos === 'after') { $num = $num.$space.$currencySymbol; diff --git a/app/Services/Documents/TokenScanner.php b/app/Services/Documents/TokenScanner.php index 2581caf..96db770 100644 --- a/app/Services/Documents/TokenScanner.php +++ b/app/Services/Documents/TokenScanner.php @@ -4,7 +4,8 @@ class TokenScanner { - private const REGEX = '/{{\s*([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\s*}}/'; + // Allow entity.attr with attr accepting letters, digits, underscore, dot and hyphen for flexibility (e.g., custom.order-id) + private const REGEX = '/{{\s*([a-zA-Z0-9_]+\.[a-zA-Z0-9_.-]+)\s*}}/'; /** * @return array diff --git a/app/Services/Documents/TokenValueResolver.php b/app/Services/Documents/TokenValueResolver.php index 9e2e299..767538a 100644 --- a/app/Services/Documents/TokenValueResolver.php +++ b/app/Services/Documents/TokenValueResolver.php @@ -13,12 +13,39 @@ class TokenValueResolver * Returns array with keys: values (resolved token=>value) and unresolved (list of tokens not resolved / not allowed) * Policy determines whether invalid tokens throw (fail) or are collected (blank|keep). * - * @return array{values:array,unresolved:array} + * @return array{values:array,unresolved:array,customTypes?:array} */ - public function resolve(array $tokens, DocumentTemplate $template, Contract $contract, User $user, string $policy = 'fail'): array - { + public function resolve( + array $tokens, + DocumentTemplate $template, + Contract $contract, + User $user, + string $policy = 'fail', + array $customOverrides = [], + ?array $customDefaults = null, + string $onMissingCustom = 'empty' + ): array { $values = []; $unresolved = []; + $customTypesOut = []; + // Custom namespace: merge defaults from settings/template meta and overrides + $settings = app(\App\Services\Documents\DocumentSettings::class)->get(); + $defaults = $customDefaults ?? ($template->meta['custom_defaults'] ?? null) ?? ($settings->custom_defaults ?? []); + if (! is_array($defaults)) { + $defaults = []; + } + if (! is_array($customOverrides)) { + $customOverrides = []; + } + $custom = array_replace($defaults, $customOverrides); + // Collect custom types from template meta (optional) + $customTypes = []; + if (isset($template->meta['custom_default_types']) && is_array($template->meta['custom_default_types'])) { + foreach ($template->meta['custom_default_types'] as $k => $t) { + $t = in_array($t, ['string', 'number', 'date'], true) ? $t : 'string'; + $customTypes[(string) $k] = $t; + } + } // Retrieve whitelist from DB settings (if present) and merge with config baseline (config acts as baseline; DB can add or override entity arrays) $settingsWhitelist = app(\App\Services\Documents\DocumentSettings::class)->get()->whitelist ?? []; $configWhitelist = config('documents.whitelist', []); @@ -37,6 +64,34 @@ public function resolve(array $tokens, DocumentTemplate $template, Contract $con continue; } + if ($entity === 'custom') { + // Track type info if present + if (isset($customTypes[$attr])) { + $customTypesOut[$token] = $customTypes[$attr]; + } + if (array_key_exists($attr, $custom)) { + $v = $custom[$attr]; + if (is_scalar($v) || (is_object($v) && method_exists($v, '__toString'))) { + $values[$token] = (string) $v; + } else { + $values[$token] = ''; + } + } else { + // Missing custom – apply onMissingCustom policy locally (empty|leave|error) + if ($onMissingCustom === 'error') { + if ($policy === 'fail') { + throw new \RuntimeException("Manjkajoč custom token: {$token}"); + } + $unresolved[] = $token; + } elseif ($onMissingCustom === 'leave') { + $unresolved[] = $token; + } else { // empty + $values[$token] = ''; + } + } + + continue; + } if (! in_array($entity, $templateEntities, true)) { if ($policy === 'fail') { throw new \RuntimeException("Nedovoljen entiteta token: $entity"); @@ -45,7 +100,12 @@ public function resolve(array $tokens, DocumentTemplate $template, Contract $con continue; } - $allowed = ($template->columns[$entity] ?? []) ?: ($globalWhitelist[$entity] ?? []); + // Allowed attributes: merge template-declared columns with global whitelist (config + DB settings) + // Rationale: old templates may not list newly allowed attributes (like nested paths), + // so we honor both sources instead of preferring one exclusively. + $allowedFromTemplate = $template->columns[$entity] ?? []; + $allowedFromGlobal = $globalWhitelist[$entity] ?? []; + $allowed = array_values(array_unique(array_merge($allowedFromTemplate, $allowedFromGlobal))); if (! in_array($attr, $allowed, true)) { if ($policy === 'fail') { throw new \RuntimeException("Nedovoljen stolpec token: $token"); @@ -57,7 +117,11 @@ public function resolve(array $tokens, DocumentTemplate $template, Contract $con $values[$token] = $this->entityAttribute($entity, $attr, $contract) ?? ''; } - return ['values' => $values, 'unresolved' => array_values(array_unique($unresolved))]; + return [ + 'values' => $values, + 'unresolved' => array_values(array_unique($unresolved)), + 'customTypes' => $customTypesOut, + ]; } private function generationAttribute(string $attr, User $user): string @@ -78,11 +142,25 @@ private function entityAttribute(string $entity, string $attr, Contract $contrac case 'client_case': return (string) optional($contract->clientCase)->{$attr}; case 'client': - return (string) optional(optional($contract->clientCase)->client)->{$attr}; - case 'person': - $person = optional(optional($contract->clientCase)->person); + $client = optional($contract->clientCase)->client; + if (! $client) { + return ''; + } + if (str_contains($attr, '.')) { + return $this->resolveNestedFromModel($client, $attr); + } - return (string) $person->{$attr}; + return (string) ($client->{$attr} ?? ''); + case 'person': + $person = optional($contract->clientCase)->person; + if (! $person) { + return ''; + } + if (str_contains($attr, '.')) { + return $this->resolveNestedFromModel($person, $attr); + } + + return (string) ($person->{$attr} ?? ''); case 'account': $account = optional($contract->account); @@ -91,4 +169,39 @@ private function entityAttribute(string $entity, string $attr, Contract $contrac return ''; } } + + /** + * Resolve nested dotted paths from a base model for supported relations/aliases. + * Supports: + * - Client: person.* + * - Person: person_address.* (uses first active address) + */ + private function resolveNestedFromModel(object $model, string $path): string + { + $segments = explode('.', $path); + $current = $model; + foreach ($segments as $seg) { + if (! $current) { + return ''; + } + if ($current instanceof \App\Models\Client && $seg === 'person') { + $current = $current->person; + + continue; + } + if ($current instanceof \App\Models\Person\Person && $seg === 'person_address') { + $current = $current->addresses()->first(); + + continue; + } + // Default attribute access + try { + $current = is_array($current) ? ($current[$seg] ?? null) : ($current->{$seg} ?? null); + } catch (\Throwable $e) { + return ''; + } + } + + return $current !== null ? (string) $current : ''; + } } diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index b24d898..6f99f5f 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -2365,13 +2365,15 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array if (! $field) { continue; } - $val = $addrData[$field] ?? null; + // Allow alias 'postal_code' in CSV mappings but persist as 'post_code' in DB + $targetField = $field === 'postal_code' ? 'post_code' : $field; + $val = $addrData[$field] ?? $addrData[$targetField] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { - $applyInsert[$field] = $val; + $applyInsert[$targetField] = $val; } if (in_array($mode, ['update', 'both'])) { - $applyUpdate[$field] = $val; + $applyUpdate[$targetField] = $val; } } if ($existing) { diff --git a/config/documents.php b/config/documents.php index ff37d20..d3a319a 100644 --- a/config/documents.php +++ b/config/documents.php @@ -20,7 +20,26 @@ 'whitelist' => [ 'contract' => ['reference', 'start_date', 'end_date', 'description'], 'client_case' => ['client_ref'], - 'client' => [], - 'person' => ['full_name', 'first_name', 'last_name', 'nu'], + 'client' => [ + 'person', + 'person.full_name', + 'person.first_name', + 'person.last_name', + 'person.nu', + 'person.person_address', + 'person.person_address.address', + 'person.person_address.post_code', + 'person.person_address.city', + ], + 'person' => [ + 'full_name', + 'first_name', + 'last_name', + 'nu', + 'person_address', + 'person_address.address', + 'person_address.post_code', + 'person_address.city', + ], ], ]; diff --git a/database/factories/Person/PersonAddressFactory.php b/database/factories/Person/PersonAddressFactory.php index b7b7968..81e9f20 100644 --- a/database/factories/Person/PersonAddressFactory.php +++ b/database/factories/Person/PersonAddressFactory.php @@ -21,6 +21,8 @@ public function definition(): array return [ 'address' => $this->faker->streetAddress(), 'country' => 'SI', + 'post_code' => $this->faker->postcode(), + 'city' => $this->faker->city(), 'type_id' => AddressType::factory(), 'user_id' => User::factory(), ]; diff --git a/database/migrations/2025_10_12_120500_add_post_code_and_city_to_person_addresses.php b/database/migrations/2025_10_12_120500_add_post_code_and_city_to_person_addresses.php new file mode 100644 index 0000000..71c202e --- /dev/null +++ b/database/migrations/2025_10_12_120500_add_post_code_and_city_to_person_addresses.php @@ -0,0 +1,28 @@ +string('post_code', 16)->nullable(); + $table->string('city', 100)->nullable(); + }); + } + + public function down(): void + { + Schema::table('person_addresses', function (Blueprint $table): void { + if (Schema::hasColumn('person_addresses', 'post_code')) { + $table->dropColumn('post_code'); + } + if (Schema::hasColumn('person_addresses', 'city')) { + $table->dropColumn('city'); + } + }); + } +}; diff --git a/database/migrations/2025_10_12_150000_add_custom_defaults_to_document_settings.php b/database/migrations/2025_10_12_150000_add_custom_defaults_to_document_settings.php new file mode 100644 index 0000000..c836ff1 --- /dev/null +++ b/database/migrations/2025_10_12_150000_add_custom_defaults_to_document_settings.php @@ -0,0 +1,26 @@ +json('custom_defaults')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('document_settings', function (Blueprint $table): void { + if (Schema::hasColumn('document_settings', 'custom_defaults')) { + $table->dropColumn('custom_defaults'); + } + }); + } +}; diff --git a/resources/js/Components/AddressCreateForm.vue b/resources/js/Components/AddressCreateForm.vue index 66dd364..1a782aa 100644 --- a/resources/js/Components/AddressCreateForm.vue +++ b/resources/js/Components/AddressCreateForm.vue @@ -1,5 +1,6 @@ - \ No newline at end of file + + + diff --git a/resources/js/Components/PersonInfoGrid.vue b/resources/js/Components/PersonInfoGrid.vue index 0ad966a..291bc68 100644 --- a/resources/js/Components/PersonInfoGrid.vue +++ b/resources/js/Components/PersonInfoGrid.vue @@ -7,6 +7,7 @@ import { provide, ref, watch } from 'vue'; import axios from 'axios'; import PersonUpdateForm from './PersonUpdateForm.vue'; import AddressCreateForm from './AddressCreateForm.vue'; +import AddressUpdateForm from './AddressUpdateForm.vue'; import PhoneCreateForm from './PhoneCreateForm.vue'; import EmailCreateForm from './EmailCreateForm.vue'; import EmailUpdateForm from './EmailUpdateForm.vue'; @@ -74,8 +75,9 @@ const closeConfirm = () => { confirm.value.show = false; }; const getMainAddress = (adresses) => { const addr = adresses.filter( a => a.type.id === 1 )[0] ?? ''; if( addr !== '' ){ + const tail = (addr.post_code && addr.city) ? `, ${addr.post_code} ${addr.city}` : ''; const country = addr.country !== '' ? ` - ${addr.country}` : ''; - return addr.address !== '' ? addr.address + country : ''; + return addr.address !== '' ? (addr.address + tail + country) : ''; } return ''; @@ -234,7 +236,9 @@ const getTRRs = (p) => { -

{{ address.address }}

+

+ {{ (address.post_code && address.city) ? `${address.address}, ${address.post_code} ${address.city}` : address.address }} +

@@ -335,6 +339,13 @@ const getTRRs = (p) => { :id="editAddressId" :edit="editAddress" /> + + +
+

+ Custom tokens (privzete vrednosti) +

+
+
+ +
+
+
+ + + + +
+
+

+ Uporabite v predlogi kot {{custom.your_key}}. Manjkajoče vrednosti se privzeto izpraznijo. +

+
+
+