Procházet zdrojové kódy

Add customer lookup to job type attributes with multi-field match support

- Replace customer_lookup "Search/display field" single dropdown with a
  free-text "Match field name(s)" input supporting multiple fields (semicolon-separated),
  matching the api_lookup UX pattern
- When customer_lookup type is selected in the job type editor, any existing
  api_lookup attributes are automatically removed (customer_lookup supersedes api_lookup)
- In the job create/edit form, api_lookup attributes are suppressed when the
  job type includes a customer_lookup (customer_lookup takes the place of api_lookup)
- Backend /customers/lookup endpoint already handled multi-field match_field;
  no PHP changes required for that path
Customer_Type_Lookup
Daniel Covington před 2 týdny
rodič
revize
4a6d34e0d2
10 změnil soubory, kde provedl 373 přidání a 60 odebrání
  1. +2
    -1
      .claude/settings.local.json
  2. +68
    -0
      app/Controllers/CustomerController.php
  3. +17
    -7
      app/Controllers/JobTypeController.php
  4. +14
    -0
      app/Repositories/CustomerRepository.php
  5. +37
    -2
      app/Views/job-types/create.php
  6. +37
    -2
      app/Views/job-types/edit.php
  7. +4
    -4
      app/Views/jobs/create.php
  8. +4
    -4
      app/Views/jobs/edit.php
  9. +189
    -40
      public/js/app.js
  10. +1
    -0
      routes/web.php

+ 2
- 1
.claude/settings.local.json Zobrazit soubor

@@ -14,7 +14,8 @@
"Bash(findstr \"^app\")",
"PowerShell(cd \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\"; Get-ChildItem -Recurse -Directory | Select-Object -ExpandProperty FullName | Where-Object { $_ -notmatch '\\\\.git|\\\\.claude|node_modules' } | Sort-Object | Select-Object -First 30)",
"Bash(git -C \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" remote get-url origin)",
"PowerShell(Get-ChildItem -Path \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" -Recurse -Directory -ErrorAction SilentlyContinue | Select-Object -First 30 | ForEach-Object { $_.FullName })"
"PowerShell(Get-ChildItem -Path \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" -Recurse -Directory -ErrorAction SilentlyContinue | Select-Object -First 30 | ForEach-Object { $_.FullName })",
"Bash(Get-ChildItem -Path \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" -Directory)"
]
}
}

+ 68
- 0
app/Controllers/CustomerController.php Zobrazit soubor

@@ -187,6 +187,74 @@ class CustomerController extends Controller
return $this->redirect('/customers?deleted=1');
}

public function lookup(): Response
{
$request = Request::capture();
$typeId = (int) $request->input('type_id', 0);
$matchParam = trim((string) $request->input('match_field', ''));

if ($typeId <= 0) {
return $this->json(['fields' => [], 'records' => []]);
}

// match_field is an attribute NAME (not alias)
$matchNames = $matchParam !== ''
? array_values(array_filter(array_map('trim', explode(';', $matchParam))))
: [];

$rows = $this->repo()->searchByType($typeId);

if (empty($rows)) {
return $this->json(['fields' => [], 'records' => []]);
}

$typeAttrs = [];
if (!empty($rows[0]['type_attributes'])) {
$typeAttrs = json_decode((string) $rows[0]['type_attributes'], true) ?? [];
}

// Collect all attribute names in order
$attrNames = [];
foreach ($typeAttrs as $attr) {
$name = trim((string) ($attr['name'] ?? ''));
if ($name !== '') {
$attrNames[] = $name;
}
}

if (empty($matchNames) && !empty($attrNames)) {
$matchNames = [$attrNames[0]];
}

$records = [];
foreach ($rows as $row) {
$attrValues = !empty($row['attribute_values'])
? (json_decode((string) $row['attribute_values'], true) ?? [])
: [];

// _row keyed by attribute NAME — matches attribute_values storage format
$rowByName = [];
foreach ($attrNames as $name) {
$rowByName[$name] = (string) ($attrValues[$name] ?? '');
}

$display = array_map(fn($n) => $rowByName[$n] ?? '', $matchNames);
$primary = $display[0] ?? '';

if ($primary === '') {
continue;
}

$records[] = [
'_primary' => $primary,
'_display' => array_values($display),
'_row' => $rowByName,
];
}

return $this->json(['fields' => $matchNames, 'records' => $records]);
}

// ── Helpers ───────────────────────────────────────────────────────────────

private function loadCustomerTypes(): array


+ 17
- 7
app/Controllers/JobTypeController.php Zobrazit soubor

@@ -223,12 +223,15 @@ class JobTypeController extends Controller
->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.')
->errors());

$attributeAliases = (array) ($request->input('attribute_alias') ?? []);
$attributeApiUrls = (array) ($request->input('attribute_api_url') ?? []);
$attributeApiFormats = (array) ($request->input('attribute_api_format') ?? []);
$attributeApiReturnTypes = (array) ($request->input('attribute_api_return_type') ?? []);
$attributeApiMatchFields = (array) ($request->input('attribute_api_match_field') ?? []);
$attributeApiAutoFills = (array) ($request->input('attribute_api_auto_fill') ?? []);
$attributeAliases = (array) ($request->input('attribute_alias') ?? []);
$attributeApiUrls = (array) ($request->input('attribute_api_url') ?? []);
$attributeApiFormats = (array) ($request->input('attribute_api_format') ?? []);
$attributeApiReturnTypes = (array) ($request->input('attribute_api_return_type') ?? []);
$attributeApiMatchFields = (array) ($request->input('attribute_api_match_field') ?? []);
$attributeApiAutoFills = (array) ($request->input('attribute_api_auto_fill') ?? []);
$attributeDbMatchFields = (array) ($request->input('attribute_db_match_field') ?? []);
$attributeDbAutoFills = (array) ($request->input('attribute_db_auto_fill') ?? []);
$attributeDbCustomerTypeIds = (array) ($request->input('attribute_db_customer_type_id') ?? []);

$attributes = [];

@@ -241,7 +244,8 @@ class JobTypeController extends Controller
// by the Job Type editor JS before submit. If one slips through, drop it.
if ($attrType === 'customer') continue;

$validatedType = in_array($attrType, ['text', 'number', 'date', 'boolean', 'api_lookup'], true) ? $attrType : 'text';
if ($attrType === 'database_lookup') $attrType = 'customer_lookup'; // renamed
$validatedType = in_array($attrType, ['text', 'number', 'date', 'boolean', 'api_lookup', 'customer_lookup'], true) ? $attrType : 'text';

$attr = [
'name' => $attrName,
@@ -262,6 +266,12 @@ class JobTypeController extends Controller
$attr['api_auto_fill'] = trim((string) ($attributeApiAutoFills[$i] ?? ''));
}

if ($validatedType === 'customer_lookup') {
$attr['db_match_field'] = trim((string) ($attributeDbMatchFields[$i] ?? ''));
$attr['db_auto_fill'] = trim((string) ($attributeDbAutoFills[$i] ?? ''));
$attr['db_customer_type_id'] = max(0, (int) ($attributeDbCustomerTypeIds[$i] ?? 0));
}

$attributes[] = $attr;
}



+ 14
- 0
app/Repositories/CustomerRepository.php Zobrazit soubor

@@ -69,6 +69,20 @@ class CustomerRepository extends Repository
);
}

/** Used after INSERT to recover the generated id for audit logging. */
/** @return list<array<string, mixed>> */
public function searchByType(int $typeId): array
{
return $this->database->query(
'SELECT c.id, c.attribute_values, ct.attributes AS type_attributes
FROM customer c
INNER JOIN customer_type ct ON c.customer_type_id = ct.id
WHERE c.customer_type_id = :type_id
ORDER BY c.id ASC',
['type_id' => $typeId]
);
}

/** Used after INSERT to recover the generated id for audit logging. */
public function findLatestByType(int $typeId): ?array
{


+ 37
- 2
app/Views/job-types/create.php Zobrazit soubor

@@ -19,7 +19,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
<div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
<?php endif; ?>

<form method="post" action="/job-types" class="ct-form" novalidate>
<form method="post" action="/job-types" class="ct-form" novalidate x-on:submit="guardSubmit($event)">
<?= csrf_field() ?>

<div class="form-section">
@@ -68,13 +68,14 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
</label>
<label class="field attribute-type-field">
<span>Type</span>
<select class="input" :name="`attribute_type[${index}]`" x-model="attr.type">
<select class="input" :name="`attribute_type[${index}]`" x-model="attr.type" x-on:change="onAttributeTypeChange(index)">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="boolean">True/False</option>
<option value="api_lookup">API Lookup</option>
<option value="customer">Customer Type</option>
<option value="customer_lookup">Customer Lookup</option>
</select>
</label>
<div class="attribute-remove">
@@ -137,6 +138,40 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
No customer types exist yet. <a href="/customer-types/create">Create one</a> first.
</p>
</div>
<div class="api-lookup-config" x-show="attr.type === 'customer_lookup'">
<p class="field-error" x-show="!attr.name.trim()" style="margin:0 0 8px">
Fill in the attribute name above before saving.
</p>
<label class="field">
<span>Customer Type to search</span>
<select class="input"
:name="`attribute_db_customer_type_id[${index}]`"
x-model.number="attr.db_customer_type_id"
x-on:change="onDbCustomerTypeChange(attr)">
<option value="0">— Select a customer type —</option>
<?php foreach ($model->customerTypes as $ct): ?>
<option value="<?= e((string) $ct['id']) ?>"><?= e($ct['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="field api-lookup-match-field" x-show="attr.db_customer_type_id > 0">
<span>Match field name(s)</span>
<input class="input" type="text"
:name="`attribute_db_match_field[${index}]`"
x-model="attr.db_match_field"
placeholder="e.g. FirstName or FirstName;LastName">
</label>
<p class="attributes-hint" x-show="attr.db_match_field && attr.db_auto_fill">
Auto-fills: <strong x-text="attr.db_auto_fill.split(';').filter(Boolean).join(', ')"></strong>
</p>
<p class="attributes-hint" x-show="attr.db_customer_type_id > 0 && getCustomerTypeAttrs(attr.db_customer_type_id).length === 0">
This customer type has no attributes defined.
</p>
<input type="hidden" :name="`attribute_db_auto_fill[${index}]`" :value="attr.db_auto_fill">
<?php if (empty($model->customerTypes)): ?>
<p class="attributes-hint">No customer types exist yet. <a href="/customer-types/create">Create one</a> first.</p>
<?php endif; ?>
</div>
</div>
</template>
</div>


+ 37
- 2
app/Views/job-types/edit.php Zobrazit soubor

@@ -26,7 +26,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
<div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
<?php endif; ?>

<form method="post" action="/job-types/<?= e((string) $jobTypeId) ?>/update" class="ct-form" novalidate>
<form method="post" action="/job-types/<?= e((string) $jobTypeId) ?>/update" class="ct-form" novalidate x-on:submit="guardSubmit($event)">
<?= csrf_field() ?>

<div class="form-section">
@@ -75,13 +75,14 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
</label>
<label class="field attribute-type-field">
<span>Type</span>
<select class="input" :name="`attribute_type[${index}]`" x-model="attr.type">
<select class="input" :name="`attribute_type[${index}]`" x-model="attr.type" x-on:change="onAttributeTypeChange(index)">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="boolean">True/False</option>
<option value="api_lookup">API Lookup</option>
<option value="customer">Customer Type</option>
<option value="customer_lookup">Customer Lookup</option>
</select>
</label>
<div class="attribute-remove">
@@ -144,6 +145,40 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
No customer types exist yet. <a href="/customer-types/create">Create one</a> first.
</p>
</div>
<div class="api-lookup-config" x-show="attr.type === 'customer_lookup'">
<p class="field-error" x-show="!attr.name.trim()" style="margin:0 0 8px">
Fill in the attribute name above before saving.
</p>
<label class="field">
<span>Customer Type to search</span>
<select class="input"
:name="`attribute_db_customer_type_id[${index}]`"
x-model.number="attr.db_customer_type_id"
x-on:change="onDbCustomerTypeChange(attr)">
<option value="0">— Select a customer type —</option>
<?php foreach ($model->customerTypes as $ct): ?>
<option value="<?= e((string) $ct['id']) ?>"><?= e($ct['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="field api-lookup-match-field" x-show="attr.db_customer_type_id > 0">
<span>Match field name(s)</span>
<input class="input" type="text"
:name="`attribute_db_match_field[${index}]`"
x-model="attr.db_match_field"
placeholder="e.g. FirstName or FirstName;LastName">
</label>
<p class="attributes-hint" x-show="attr.db_match_field && attr.db_auto_fill">
Auto-fills: <strong x-text="attr.db_auto_fill.split(';').filter(Boolean).join(', ')"></strong>
</p>
<p class="attributes-hint" x-show="attr.db_customer_type_id > 0 && getCustomerTypeAttrs(attr.db_customer_type_id).length === 0">
This customer type has no attributes defined.
</p>
<input type="hidden" :name="`attribute_db_auto_fill[${index}]`" :value="attr.db_auto_fill">
<?php if (empty($model->customerTypes)): ?>
<p class="attributes-hint">No customer types exist yet. <a href="/customer-types/create">Create one</a> first.</p>
<?php endif; ?>
</div>
</div>
</template>
</div>


+ 4
- 4
app/Views/jobs/create.php Zobrazit soubor

@@ -76,7 +76,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div>
<div class="form-grid">
<template x-for="attr in currentAttributes" :key="attr.name">
<label class="field" :class="{ 'api-lookup-label': attr.type === 'api_lookup' }">
<label class="field" :class="{ 'api-lookup-label': attr.type === 'api_lookup' || attr.type === 'customer_lookup' }">
<span x-text="attr.name"></span>
<template x-if="attr.type === 'boolean'">
<select class="input"
@@ -87,12 +87,12 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
<option value="false" :selected="attributeValues[attr.name] === 'false'">False</option>
</select>
</template>
<template x-if="attr.type === 'api_lookup'">
<template x-if="attr.type === 'api_lookup' || attr.type === 'customer_lookup'">
<div class="api-lookup-field">
<input class="input" type="text"
:name="`attribute_values[${attr.name}]`"
x-model="attributeValues[attr.name]"
placeholder="Type to search…"
:placeholder="attr.type === 'customer_lookup' ? 'Type to search customers…' : 'Type to search…'"
autocomplete="off"
x-on:focus="openApiLookup(attr.name)"
x-on:blur="closeApiLookup(attr.name)"
@@ -126,7 +126,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div>
</div>
</template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup' && attr.type !== 'customer_lookup'">
<input class="input" :type="inputType(attr.type)"
:name="`attribute_values[${attr.name}]`"
:value="attributeValues[attr.name] ?? ''"


+ 4
- 4
app/Views/jobs/edit.php Zobrazit soubor

@@ -73,7 +73,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div>
<div class="form-grid">
<template x-for="attr in currentAttributes" :key="attr.name">
<label class="field" :class="{ 'api-lookup-label': attr.type === 'api_lookup' }">
<label class="field" :class="{ 'api-lookup-label': attr.type === 'api_lookup' || attr.type === 'customer_lookup' }">
<span x-text="attr.name"></span>
<template x-if="attr.type === 'boolean'">
<select class="input"
@@ -84,12 +84,12 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
<option value="false" :selected="attributeValues[attr.name] === 'false'">False</option>
</select>
</template>
<template x-if="attr.type === 'api_lookup'">
<template x-if="attr.type === 'api_lookup' || attr.type === 'customer_lookup'">
<div class="api-lookup-field">
<input class="input" type="text"
:name="`attribute_values[${attr.name}]`"
x-model="attributeValues[attr.name]"
placeholder="Type to search…"
:placeholder="attr.type === 'customer_lookup' ? 'Type to search customers…' : 'Type to search…'"
autocomplete="off"
x-on:focus="openApiLookup(attr.name)"
x-on:blur="closeApiLookup(attr.name)"
@@ -123,7 +123,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div>
</div>
</template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup' && attr.type !== 'customer_lookup'">
<input class="input" :type="inputType(attr.type)"
:name="`attribute_values[${attr.name}]`"
:value="attributeValues[attr.name] ?? ''"


+ 189
- 40
public/js/app.js Zobrazit soubor

@@ -1183,6 +1183,8 @@ window.jobTypeForm = function (initialAttributes, customerTypes) {
name: '', type: 'text', alias: '', order: this.attributes.length + 1,
api_url: '', api_match_field: '', api_auto_fill: '', api_format: 'json', api_return_type: 'text',
customer_type_id: 0,
db_match_field: '', db_auto_fill: '', db_customer_type_id: 0,
_uid: Math.random().toString(36).slice(2), _imported_by: '',
});
},

@@ -1203,16 +1205,20 @@ window.jobTypeForm = function (initialAttributes, customerTypes) {
return (a.order || 0) - (b.order || 0);
}).map(function (a) {
return {
name: a.name || '',
type: a.type || 'text',
alias: a.alias || '',
order: 0,
api_url: a.api_url || '',
api_match_field: a.api_match_field || '',
api_auto_fill: a.api_auto_fill || '',
api_format: a.api_format || 'json',
api_return_type: a.api_return_type || 'text',
customer_type_id: 0,
name: a.name || '',
type: a.type || 'text',
alias: a.alias || '',
order: 0,
api_url: a.api_url || '',
api_match_field: a.api_match_field || '',
api_auto_fill: a.api_auto_fill || '',
api_format: a.api_format || 'json',
api_return_type: a.api_return_type || 'text',
customer_type_id: 0,
db_match_field: a.db_match_field || '',
db_auto_fill: a.db_auto_fill || '',
db_customer_type_id: 0,
_uid: Math.random().toString(36).slice(2), _imported_by: '',
};
});

@@ -1258,6 +1264,84 @@ window.jobTypeForm = function (initialAttributes, customerTypes) {
event.target.submit();
}
},

getCustomerTypeAttrs(customerTypeId) {
var ct = this.customerTypes.find(function (c) { return Number(c.id) === Number(customerTypeId); });
if (!ct || !Array.isArray(ct.attributes)) return [];
return ct.attributes
.filter(function (a) { return a.name && a.name.trim(); })
.slice()
.sort(function (a, b) { return (a.order || 0) - (b.order || 0); });
},

onDbCustomerTypeChange(attr) {
var self = this;

if (!attr._uid) { attr._uid = Math.random().toString(36).slice(2); }
var uid = attr._uid;

// Remove rows previously imported by this lookup (current session only)
for (var i = self.attributes.length - 1; i >= 0; i--) {
if (self.attributes[i]._imported_by === uid) {
self.attributes.splice(i, 1);
}
}

attr.db_match_field = '';
attr.db_auto_fill = '';

var ct = self.customerTypes.find(function (c) { return Number(c.id) === Number(attr.db_customer_type_id); });
if (!ct) return;

if (!attr.name.trim()) { attr.name = ct.name; }

var ctAttrs = self.getCustomerTypeAttrs(attr.db_customer_type_id);
if (ctAttrs.length === 0) return;

// db_match_field and db_auto_fill use attribute NAMES (not aliases)
// so they work regardless of whether aliases are configured
attr.db_match_field = ctAttrs[0].name;
attr.db_auto_fill = ctAttrs.map(function (a) { return a.name; }).join(';');

// Import ALL CT attributes as regular rows after this lookup row
var lookupIndex = self.attributes.indexOf(attr);
var rows = ctAttrs.map(function (a) {
var type = a.type || 'text';
if (type === 'customer_lookup' || type === 'database_lookup') { type = 'text'; }
return {
name: a.name || '', type: type, alias: a.alias || '', order: 0,
api_url: a.api_url || '', api_match_field: a.api_match_field || '',
api_auto_fill: a.api_auto_fill || '', api_format: a.api_format || 'json',
api_return_type: a.api_return_type || 'text',
customer_type_id: 0, db_match_field: '', db_auto_fill: '', db_customer_type_id: 0,
_uid: Math.random().toString(36).slice(2), _imported_by: uid,
};
});
self.attributes.splice.apply(self.attributes, [lookupIndex + 1, 0].concat(rows));
self.renumberOrder();
},

onAttributeTypeChange(index) {
var self = this;
var attr = self.attributes[index];
if (!attr || attr.type !== 'customer_lookup') return;
for (var i = self.attributes.length - 1; i >= 0; i--) {
if (self.attributes[i] !== attr && self.attributes[i].type === 'api_lookup') {
self.attributes.splice(i, 1);
}
}
self.renumberOrder();
},

guardSubmit(event) {
var missing = this.attributes.filter(function (a) {
return a.type === 'customer_lookup' && !a.name.trim();
});
if (missing.length > 0) {
event.preventDefault();
alert('One or more Database Lookup attributes are missing a name. Fill in the attribute name field before saving.');
}
},
};
};

@@ -1454,15 +1538,21 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {

get currentAttributes() {
if (!this.currentType) return [];
return this.currentType.attributes.slice().sort(function (a, b) {
var attrs = this.currentType.attributes.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
});
var hasCustomerLookup = attrs.some(function (a) { return a.type === 'customer_lookup'; });
if (hasCustomerLookup) {
attrs = attrs.filter(function (a) { return a.type !== 'api_lookup'; });
}
return attrs;
},

init() {
var self = this;
this.currentAttributes.forEach(function (attr) {
if (attr.type === 'api_lookup') { self.fetchApiValue(attr); }
if (attr.type === 'api_lookup') { self.fetchApiValue(attr); }
if (attr.type === 'customer_lookup') { self.fetchDbLookupValue(attr); }
});
},

@@ -1474,7 +1564,8 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
var self = this;
this.$nextTick(function () {
self.currentAttributes.forEach(function (attr) {
if (attr.type === 'api_lookup') { self.fetchApiValue(attr); }
if (attr.type === 'api_lookup') { self.fetchApiValue(attr); }
if (attr.type === 'customer_lookup') { self.fetchDbLookupValue(attr); }
});
});
},
@@ -1512,19 +1603,31 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
var newValues = Object.assign({}, this.attributeValues);
newValues[attr.name] = rec._primary;

var autoFill = (attr.api_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
var attrs = this.currentAttributes;
autoFill.forEach(function (alias) {
var target = null;
for (var i = 0; i < attrs.length; i++) {
if (attrs[i].alias === alias) { target = attrs[i]; break; }
}
if (!target) return;
var rowVal = rec._row[alias];
if (rowVal !== undefined && rowVal !== null) {
newValues[target.name] = String(rowVal);
}
});
if (attr.type === 'customer_lookup') {
// Auto-fill by attribute NAME: _row is name-keyed, db_auto_fill stores names
var names = (attr.db_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
names.forEach(function (name) {
var rowVal = rec._row[name];
if (rowVal !== undefined && rowVal !== null) {
newValues[name] = String(rowVal);
}
});
} else {
// api_lookup: auto-fill by alias
var autoFill = (attr.api_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
var attrs = this.currentAttributes;
autoFill.forEach(function (alias) {
var target = null;
for (var i = 0; i < attrs.length; i++) {
if (attrs[i].alias === alias) { target = attrs[i]; break; }
}
if (!target) return;
var rowVal = rec._row[alias];
if (rowVal !== undefined && rowVal !== null) {
newValues[target.name] = String(rowVal);
}
});
}

this.attributeValues = newValues;
this.closeApiLookup(attr.name);
@@ -1623,6 +1726,40 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
});
},

fetchDbLookupValue(attr) {
var self = this;
var typeId = Number(attr.db_customer_type_id || 0);
if (!typeId) return;

var matchField = attr.db_match_field || '';
var url = '/customers/lookup?type_id=' + typeId;
if (matchField) { url += '&match_field=' + encodeURIComponent(matchField); }

// Feed into the same apiLookup* state so customer_lookup uses the api_lookup UI
var s = {}; s[attr.name] = 'loading';
var e = {}; e[attr.name] = '';
var o = {}; o[attr.name] = { fields: [], records: [] };
self.apiLookupState = Object.assign({}, self.apiLookupState, s);
self.apiLookupError = Object.assign({}, self.apiLookupError, e);
self.apiLookupOptions = Object.assign({}, self.apiLookupOptions, o);

fetch(url, { headers: { Accept: 'application/json' } })
.then(function (res) { return res.json(); })
.then(function (data) {
var oo = {}; oo[attr.name] = { fields: data.fields || [], records: data.records || [] };
var os = {}; os[attr.name] = 'idle';
self.apiLookupOptions = Object.assign({}, self.apiLookupOptions, oo);
self.apiLookupState = Object.assign({}, self.apiLookupState, os);
})
.catch(function (err) {
console.error('[customer-lookup] fetch failed:', err);
var ce = {}; ce[attr.name] = 'Network error — see browser console.';
var cs = {}; cs[attr.name] = 'error';
self.apiLookupError = Object.assign({}, self.apiLookupError, ce);
self.apiLookupState = Object.assign({}, self.apiLookupState, cs);
});
},

confirmDelete(event) {
if (confirm('Delete this job? This cannot be undone.')) {
event.target.submit();
@@ -1712,7 +1849,7 @@ window.customerTypeForm = function (initialAttributes) {
dragOverIndex: null,

addAttribute() {
this.attributes.push({ name: '', type: 'text', alias: '', order: this.attributes.length + 1, api_url: '', api_match_field: '', api_auto_fill: '', api_format: 'json', api_return_type: 'text' });
this.attributes.push({ name: '', type: 'text', alias: '', order: this.attributes.length + 1, api_url: '', api_match_field: '', api_auto_fill: '', api_format: 'json', api_return_type: 'text', db_match_field: '', db_auto_fill: '' });
},

removeAttribute(index) {
@@ -2002,19 +2139,31 @@ window.customerForm = function (customerTypes, initialTypeId, initialValues) {
var newValues = Object.assign({}, this.attributeValues);
newValues[attr.name] = rec._primary;

var autoFill = (attr.api_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
var attrs = this.currentAttributes;
autoFill.forEach(function (alias) {
var target = null;
for (var i = 0; i < attrs.length; i++) {
if (attrs[i].alias === alias) { target = attrs[i]; break; }
}
if (!target) return;
var rowVal = rec._row[alias];
if (rowVal !== undefined && rowVal !== null) {
newValues[target.name] = String(rowVal);
}
});
if (attr.type === 'customer_lookup') {
// Auto-fill by attribute NAME: _row is name-keyed, db_auto_fill stores names
var names = (attr.db_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
names.forEach(function (name) {
var rowVal = rec._row[name];
if (rowVal !== undefined && rowVal !== null) {
newValues[name] = String(rowVal);
}
});
} else {
// api_lookup: auto-fill by alias
var autoFill = (attr.api_auto_fill || '').split(';').map(function (s) { return s.trim(); }).filter(Boolean);
var attrs = this.currentAttributes;
autoFill.forEach(function (alias) {
var target = null;
for (var i = 0; i < attrs.length; i++) {
if (attrs[i].alias === alias) { target = attrs[i]; break; }
}
if (!target) return;
var rowVal = rec._row[alias];
if (rowVal !== undefined && rowVal !== null) {
newValues[target.name] = String(rowVal);
}
});
}

this.attributeValues = newValues;
this.closeApiLookup(attr.name);


+ 1
- 0
routes/web.php Zobrazit soubor

@@ -74,6 +74,7 @@ $router->post('/job-types/{id}/delete', [JobTypeController::class, 'destroy'])->
// ── Customers ─────────────────────────────────────────────────────────────────
$router->get('/customers', [CustomerController::class, 'index']) ->middleware('auth');
$router->get('/customers/data', [CustomerController::class, 'data']) ->middleware('auth');
$router->get('/customers/lookup', [CustomerController::class, 'lookup']) ->middleware('auth');
$router->get('/customers/create', [CustomerController::class, 'create']) ->middleware('auth');
$router->post('/customers', [CustomerController::class, 'store']) ->middleware('auth');
$router->get('/customers/{id}/edit', [CustomerController::class, 'edit']) ->middleware('auth');


Načítá se…
Zrušit
Uložit

Powered by TurnKey Linux.