documents
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -25,4 +25,16 @@ public function fresh(): DocumentSetting
|
||||
{
|
||||
return $this->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience accessor for custom defaults.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function customDefaults(): array
|
||||
{
|
||||
$settings = $this->get();
|
||||
|
||||
return is_array($settings->custom_defaults ?? null) ? $settings->custom_defaults : [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<int,string>
|
||||
|
||||
@@ -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<string,string>,unresolved:array<int,string>}
|
||||
* @return array{values:array<string,string>,unresolved:array<int,string>,customTypes?:array<string,string>}
|
||||
*/
|
||||
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 : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user