Просмотр исходного кода

customer_api_test (#3)

coerce api_lookup customer attrs to text when imported into job type

When a customer_lookup attribute imports fields from a customer type,
any api_lookup fields are flattened to plain text — the value comes
from the selected customer record, so the lookup UI is not needed

add customer API endpoints and customer_lookup job attribute

- Add GET /api/customers, /api/customers/{id}, /api/customer-types endpoints
- Add allByTypeWithType() and include api_match_field in customer queries
- Add customer_lookup attribute type to job types: stores customer_type_id
  and imports the customer type's attributes as real fields at design time
- Job form: customer_lookup renders a searchable dropdown that fetches
  /api/customers?customer_type_id=N and auto-fills all matching attribute
  values when a customer is selected

customer-api working now

Co-authored-by: Daniel Covington <danielc@kentcommunications.com>
pull/4/head
dcovington 2 недель назад
Родитель
Сommit
8c4c8a68f1
10 измененных файлов: 351 добавлений и 43 удалений
  1. +76
    -0
      app/Controllers/CustomerApiController.php
  2. +19
    -13
      app/Controllers/JobTypeController.php
  3. +21
    -2
      app/Repositories/CustomerRepository.php
  4. +4
    -2
      app/Views/job-types/create.php
  5. +4
    -2
      app/Views/job-types/edit.php
  6. +40
    -2
      app/Views/jobs/create.php
  7. +40
    -2
      app/Views/jobs/edit.php
  8. +32
    -0
      database/migrations/20260518_000001_add_api_match_field_to_customer_type.php
  9. +109
    -20
      public/js/app.js
  10. +6
    -0
      routes/web.php

+ 76
- 0
app/Controllers/CustomerApiController.php Просмотреть файл

@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Repositories\CustomerRepository;
use App\Repositories\CustomerTypeRepository;
use Core\Controller;
use Core\Request;
use Core\Response;

class CustomerApiController extends Controller
{
public function customers(): Response
{
$request = Request::capture();
$customerTypeId = (int) ($request->input('customer_type_id') ?? 0);

$repo = new CustomerRepository(database());
$rows = $customerTypeId > 0
? $repo->allByTypeWithType($customerTypeId)
: $repo->allWithType();

return $this->json(array_map([$this, 'formatCustomer'], $rows));
}

public function customer(string $id): Response
{
$repo = new CustomerRepository(database());
$row = $repo->findWithType((int) $id);

if ($row === null) {
return Response::json(['error' => 'Not found'], 404);
}

return $this->json($this->formatCustomer($row));
}

public function customerTypes(): Response
{
$repo = new CustomerTypeRepository(database());
$rows = $repo->allOrderedByName();

$data = array_map(static function (array $row): array {
$attributes = [];
if (!empty($row['attributes'])) {
$attributes = json_decode((string) $row['attributes'], true) ?? [];
}

return [
'id' => (int) $row['id'],
'name' => (string) $row['name'],
'api_match_field' => (string) ($row['api_match_field'] ?? ''),
'attributes' => $attributes,
];
}, $rows);

return $this->json($data);
}

private function formatCustomer(array $row): array
{
$attributeValues = [];
if (!empty($row['attribute_values'])) {
$attributeValues = json_decode((string) $row['attribute_values'], true) ?? [];
}

return [
'id' => (int) $row['id'],
'customer_type_id' => (int) $row['customer_type_id'],
'customer_type_name' => (string) ($row['customer_type_name'] ?? ''),
'attribute_values' => $attributeValues,
];
}
}

+ 19
- 13
app/Controllers/JobTypeController.php Просмотреть файл

@@ -223,12 +223,13 @@ class JobTypeController extends Controller
->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.') ->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.')
->errors()); ->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') ?? []);
$attributeCustomerTypeIds = (array) ($request->input('attribute_customer_type_id') ?? []);


$attributes = []; $attributes = [];


@@ -237,11 +238,10 @@ class JobTypeController extends Controller
$attrType = trim((string) ($attributeTypes[$i] ?? 'text')); $attrType = trim((string) ($attributeTypes[$i] ?? 'text'));
if ($attrName === '') continue; if ($attrName === '') continue;


// 'customer' is a design-time placeholder that is expanded into real attribute rows
// by the Job Type editor JS before submit. If one slips through, drop it.
// 'customer' is a legacy design-time placeholder (now replaced by customer_lookup). Drop if it slips through.
if ($attrType === 'customer') continue; if ($attrType === 'customer') continue;


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


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


if ($validatedType === 'customer_lookup') {
$attr['customer_type_id'] = (int) ($attributeCustomerTypeIds[$i] ?? 0);
$attr['api_match_field'] = trim((string) ($attributeApiMatchFields[$i] ?? ''));
}

$attributes[] = $attr; $attributes[] = $attr;
} }


@@ -289,14 +294,15 @@ class JobTypeController extends Controller
return new CustomerTypeRepository(database()); return new CustomerTypeRepository(database());
} }


/** @return list<array{id: int, name: string, attributes: array}> */
/** @return list<array{id: int, name: string, api_match_field: string, attributes: array}> */
private function loadCustomerTypes(): array private function loadCustomerTypes(): array
{ {
return array_map(static function (array $t): array { return array_map(static function (array $t): array {
return [ return [
'id' => (int) $t['id'],
'name' => (string) $t['name'],
'attributes' => json_decode((string) ($t['attributes'] ?? '[]'), true) ?? [],
'id' => (int) $t['id'],
'name' => (string) $t['name'],
'api_match_field' => (string) ($t['api_match_field'] ?? ''),
'attributes' => json_decode((string) ($t['attributes'] ?? '[]'), true) ?? [],
]; ];
}, $this->customerTypeRepo()->allOrderedByName()); }, $this->customerTypeRepo()->allOrderedByName());
} }


+ 21
- 2
app/Repositories/CustomerRepository.php Просмотреть файл

@@ -48,20 +48,39 @@ class CustomerRepository extends Repository
'SELECT c.id, c.customer_type_id, c.attribute_values, 'SELECT c.id, c.customer_type_id, c.attribute_values,
c.created_at, c.updated_at, c.created_at, c.updated_at,
ct.name AS customer_type_name, ct.name AS customer_type_name,
ct.attributes AS customer_type_attributes
ct.attributes AS customer_type_attributes,
ct.api_match_field
FROM customer c FROM customer c
INNER JOIN customer_type ct ON c.customer_type_id = ct.id INNER JOIN customer_type ct ON c.customer_type_id = ct.id
ORDER BY c.id DESC' ORDER BY c.id DESC'
); );
} }


/** @return list<array<string, mixed>> */
public function allByTypeWithType(int $typeId): array
{
return $this->database->query(
'SELECT c.id, c.customer_type_id, c.attribute_values,
c.created_at, c.updated_at,
ct.name AS customer_type_name,
ct.attributes AS customer_type_attributes,
ct.api_match_field
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 DESC',
['type_id' => $typeId]
);
}

public function findWithType(int $id): ?array public function findWithType(int $id): ?array
{ {
return $this->database->first( return $this->database->first(
'SELECT c.id, c.customer_type_id, c.attribute_values, 'SELECT c.id, c.customer_type_id, c.attribute_values,
c.created_at, c.updated_at, c.created_at, c.updated_at,
ct.name AS customer_type_name, ct.name AS customer_type_name,
ct.attributes AS customer_type_attributes
ct.attributes AS customer_type_attributes,
ct.api_match_field
FROM customer c FROM customer c
INNER JOIN customer_type ct ON c.customer_type_id = ct.id INNER JOIN customer_type ct ON c.customer_type_id = ct.id
WHERE c.id = :id', WHERE c.id = :id',


+ 4
- 2
app/Views/job-types/create.php Просмотреть файл

@@ -74,7 +74,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
<option value="date">Date</option> <option value="date">Date</option>
<option value="boolean">True/False</option> <option value="boolean">True/False</option>
<option value="api_lookup">API Lookup</option> <option value="api_lookup">API Lookup</option>
<option value="customer">Customer Type</option>
<option value="customer_lookup">Customer Lookup</option>
</select> </select>
</label> </label>
<div class="attribute-remove"> <div class="attribute-remove">
@@ -121,7 +121,9 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
</select> </select>
</label> </label>
</div> </div>
<div class="customer-type-config" x-show="attr.type === 'customer'">
<div class="customer-type-config" x-show="attr.type === 'customer_lookup'">
<input type="hidden" :name="`attribute_customer_type_id[${index}]`" :value="attr.customer_type_id || 0">
<input type="hidden" :name="`attribute_api_match_field[${index}]`" :value="attr.api_match_field || ''">
<label class="field"> <label class="field">
<span>Customer Type</span> <span>Customer Type</span>
<select class="input" <select class="input"


+ 4
- 2
app/Views/job-types/edit.php Просмотреть файл

@@ -81,7 +81,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
<option value="date">Date</option> <option value="date">Date</option>
<option value="boolean">True/False</option> <option value="boolean">True/False</option>
<option value="api_lookup">API Lookup</option> <option value="api_lookup">API Lookup</option>
<option value="customer">Customer Type</option>
<option value="customer_lookup">Customer Lookup</option>
</select> </select>
</label> </label>
<div class="attribute-remove"> <div class="attribute-remove">
@@ -128,7 +128,9 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T
</select> </select>
</label> </label>
</div> </div>
<div class="customer-type-config" x-show="attr.type === 'customer'">
<div class="customer-type-config" x-show="attr.type === 'customer_lookup'">
<input type="hidden" :name="`attribute_customer_type_id[${index}]`" :value="attr.customer_type_id || 0">
<input type="hidden" :name="`attribute_api_match_field[${index}]`" :value="attr.api_match_field || ''">
<label class="field"> <label class="field">
<span>Customer Type</span> <span>Customer Type</span>
<select class="input" <select class="input"


+ 40
- 2
app/Views/jobs/create.php Просмотреть файл

@@ -76,7 +76,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div> </div>
<div class="form-grid"> <div class="form-grid">
<template x-for="attr in currentAttributes" :key="attr.name"> <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> <span x-text="attr.name"></span>
<template x-if="attr.type === 'boolean'"> <template x-if="attr.type === 'boolean'">
<select class="input" <select class="input"
@@ -126,7 +126,45 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div> </div>
</div> </div>
</template> </template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
<template x-if="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="Search customers…"
autocomplete="off"
x-on:focus="openCustomerLookup(attr.name)"
x-on:blur="closeCustomerLookup(attr.name)"
x-on:keydown.escape.prevent="closeCustomerLookup(attr.name)">
<div class="api-lookup-dropdown"
x-show="customerLookupOpen[attr.name]"
x-on:mousedown.prevent>
<p class="api-lookup-loading" x-show="customerLookupState[attr.name] === 'loading'">Loading…</p>
<div class="api-lookup-table" x-show="customerLookupState[attr.name] !== 'loading'">
<div class="api-lookup-thead"
:style="`grid-template-columns: repeat(${getCustomerOptions(attr.name).fields.length || 1}, 1fr)`">
<template x-for="f in getCustomerOptions(attr.name).fields" :key="f">
<span x-text="f"></span>
</template>
</div>
<div class="api-lookup-tbody">
<template x-for="rec in getFilteredCustomers(attr.name, attributeValues[attr.name])" :key="rec._primary">
<div class="api-lookup-tr"
:class="{ 'is-selected': attributeValues[attr.name] === rec._primary }"
:style="`grid-template-columns: repeat(${getCustomerOptions(attr.name).fields.length || 1}, 1fr)`"
x-on:click="selectCustomer(attr, rec)">
<template x-for="(v, i) in rec._display" :key="i">
<span x-text="v"></span>
</template>
</div>
</template>
<div class="api-lookup-empty" x-show="!getFilteredCustomers(attr.name, attributeValues[attr.name]).length">No customers found.</div>
</div>
</div>
</div>
</div>
</template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup' && attr.type !== 'customer_lookup'">
<input class="input" :type="inputType(attr.type)" <input class="input" :type="inputType(attr.type)"
:name="`attribute_values[${attr.name}]`" :name="`attribute_values[${attr.name}]`"
:value="attributeValues[attr.name] ?? ''" :value="attributeValues[attr.name] ?? ''"


+ 40
- 2
app/Views/jobs/edit.php Просмотреть файл

@@ -73,7 +73,7 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div> </div>
<div class="form-grid"> <div class="form-grid">
<template x-for="attr in currentAttributes" :key="attr.name"> <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> <span x-text="attr.name"></span>
<template x-if="attr.type === 'boolean'"> <template x-if="attr.type === 'boolean'">
<select class="input" <select class="input"
@@ -123,7 +123,45 @@ window.__initialJtVals = <?= json_encode($model->form['attribute_values'], JSON_
</div> </div>
</div> </div>
</template> </template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
<template x-if="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="Search customers…"
autocomplete="off"
x-on:focus="openCustomerLookup(attr.name)"
x-on:blur="closeCustomerLookup(attr.name)"
x-on:keydown.escape.prevent="closeCustomerLookup(attr.name)">
<div class="api-lookup-dropdown"
x-show="customerLookupOpen[attr.name]"
x-on:mousedown.prevent>
<p class="api-lookup-loading" x-show="customerLookupState[attr.name] === 'loading'">Loading…</p>
<div class="api-lookup-table" x-show="customerLookupState[attr.name] !== 'loading'">
<div class="api-lookup-thead"
:style="`grid-template-columns: repeat(${getCustomerOptions(attr.name).fields.length || 1}, 1fr)`">
<template x-for="f in getCustomerOptions(attr.name).fields" :key="f">
<span x-text="f"></span>
</template>
</div>
<div class="api-lookup-tbody">
<template x-for="rec in getFilteredCustomers(attr.name, attributeValues[attr.name])" :key="rec._primary">
<div class="api-lookup-tr"
:class="{ 'is-selected': attributeValues[attr.name] === rec._primary }"
:style="`grid-template-columns: repeat(${getCustomerOptions(attr.name).fields.length || 1}, 1fr)`"
x-on:click="selectCustomer(attr, rec)">
<template x-for="(v, i) in rec._display" :key="i">
<span x-text="v"></span>
</template>
</div>
</template>
<div class="api-lookup-empty" x-show="!getFilteredCustomers(attr.name, attributeValues[attr.name]).length">No customers found.</div>
</div>
</div>
</div>
</div>
</template>
<template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup' && attr.type !== 'customer_lookup'">
<input class="input" :type="inputType(attr.type)" <input class="input" :type="inputType(attr.type)"
:name="`attribute_values[${attr.name}]`" :name="`attribute_values[${attr.name}]`"
:value="attributeValues[attr.name] ?? ''" :value="attributeValues[attr.name] ?? ''"


+ 32
- 0
database/migrations/20260518_000001_add_api_match_field_to_customer_type.php Просмотреть файл

@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

use Core\Database;
use Core\Migration;

return new class extends Migration
{
public function up(Database $database): void
{
$columnExists = $database->first(
"SELECT 1 AS col FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'customer_type' AND COLUMN_NAME = 'api_match_field'"
);

if ($columnExists) {
return;
}

$database->execute(
"ALTER TABLE customer_type ADD api_match_field NVARCHAR(255) NULL"
);
}

public function down(Database $database): void
{
$database->execute(
"ALTER TABLE customer_type DROP COLUMN api_match_field"
);
}
};

+ 109
- 20
public/js/app.js Просмотреть файл

@@ -1202,21 +1202,29 @@ window.jobTypeForm = function (initialAttributes, customerTypes) {
var imported = ct.attributes.slice().sort(function (a, b) { var imported = ct.attributes.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0); return (a.order || 0) - (b.order || 0);
}).map(function (a) { }).map(function (a) {
var importedType = (a.type === 'api_lookup') ? 'text' : (a.type || 'text');
return { 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',
name: a.name || '',
type: importedType,
alias: a.alias || '',
order: 0,
api_url: '',
api_match_field: '',
api_auto_fill: '',
api_format: a.api_format || 'json',
api_return_type: a.api_return_type || 'text',
customer_type_id: 0, customer_type_id: 0,
}; };
}); });


this.attributes.splice.apply(this.attributes, [index, 1].concat(imported));
if (row.type === 'customer_lookup') {
// Keep the lookup row; stamp api_match_field from the customer type, then insert attrs after it.
row.api_match_field = ct.api_match_field || '';
this.attributes.splice.apply(this.attributes, [index + 1, 0].concat(imported));
} else {
// Legacy customer placeholder — replace with expanded attrs.
this.attributes.splice.apply(this.attributes, [index, 1].concat(imported));
}
this.renumberOrder(); this.renumberOrder();
}, },


@@ -1441,10 +1449,13 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
jobTypes: jobTypes, jobTypes: jobTypes,
selectedTypeId: String(initialTypeId || ''), selectedTypeId: String(initialTypeId || ''),
attributeValues: Object.assign({}, initialValues || {}), attributeValues: Object.assign({}, initialValues || {}),
apiLookupState: {},
apiLookupError: {},
apiLookupOptions: {},
apiLookupOpen: {},
apiLookupState: {},
apiLookupError: {},
apiLookupOptions: {},
apiLookupOpen: {},
customerLookupState: {},
customerLookupOptions: {},
customerLookupOpen: {},


get currentType() { get currentType() {
var id = this.selectedTypeId; var id = this.selectedTypeId;
@@ -1462,19 +1473,24 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
init() { init() {
var self = this; var self = this;
this.currentAttributes.forEach(function (attr) { 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.fetchCustomers(attr); }
}); });
}, },


onTypeChange() { onTypeChange() {
this.attributeValues = {};
this.apiLookupOptions = {};
this.apiLookupState = {};
this.apiLookupOpen = {};
this.attributeValues = {};
this.apiLookupOptions = {};
this.apiLookupState = {};
this.apiLookupOpen = {};
this.customerLookupOptions = {};
this.customerLookupState = {};
this.customerLookupOpen = {};
var self = this; var self = this;
this.$nextTick(function () { this.$nextTick(function () {
self.currentAttributes.forEach(function (attr) { 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.fetchCustomers(attr); }
}); });
}); });
}, },
@@ -1623,6 +1639,79 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) {
}); });
}, },


getCustomerOptions(name) {
return this.customerLookupOptions[name] || { fields: [], records: [] };
},

openCustomerLookup(name) {
var o = {}; o[name] = true;
this.customerLookupOpen = Object.assign({}, this.customerLookupOpen, o);
},

closeCustomerLookup(name) {
var o = {}; o[name] = false;
this.customerLookupOpen = Object.assign({}, this.customerLookupOpen, o);
},

getFilteredCustomers(name, search) {
var records = this.getCustomerOptions(name).records;
if (!search) return records;
var term = String(search).toLowerCase();
return records.filter(function (rec) {
return rec._display.some(function (v) {
return String(v).toLowerCase().indexOf(term) !== -1;
});
});
},

selectCustomer(attr, customer) {
var newValues = Object.assign({}, this.attributeValues);
newValues[attr.name] = customer._primary;
var av = customer._raw.attribute_values || {};
Object.keys(av).forEach(function (key) {
newValues[key] = String(av[key] !== null && av[key] !== undefined ? av[key] : '');
});
this.attributeValues = newValues;
this.closeCustomerLookup(attr.name);
},

fetchCustomers(attr) {
var self = this;
var ctId = Number(attr.customer_type_id || 0);
var matchField = attr.api_match_field || '';
if (!ctId) return;

var s = {}; s[attr.name] = 'loading';
var o = {}; o[attr.name] = { fields: [], records: [] };
self.customerLookupState = Object.assign({}, self.customerLookupState, s);
self.customerLookupOptions = Object.assign({}, self.customerLookupOptions, o);

fetch('/api/customers?customer_type_id=' + ctId, { headers: { Accept: 'application/json' } })
.then(function (res) { return res.json(); })
.then(function (rows) {
var ss = {}; ss[attr.name] = 'idle';
if (!Array.isArray(rows) || rows.length === 0) {
self.customerLookupState = Object.assign({}, self.customerLookupState, ss);
return;
}
var attrKeys = Object.keys(rows[0].attribute_values || {});
var records = rows.map(function (c) {
var av = c.attribute_values || {};
var display = attrKeys.map(function (k) { return av[k] !== undefined && av[k] !== null ? String(av[k]) : ''; });
var primary = matchField && av[matchField] !== undefined ? String(av[matchField]) : String(c.id);
return { _primary: primary, _display: display, _raw: c };
});
var oo = {}; oo[attr.name] = { fields: attrKeys, records: records };
self.customerLookupOptions = Object.assign({}, self.customerLookupOptions, oo);
self.customerLookupState = Object.assign({}, self.customerLookupState, ss);
})
.catch(function (err) {
console.error('[customer-lookup] fetch failed:', err);
var cs = {}; cs[attr.name] = 'error';
self.customerLookupState = Object.assign({}, self.customerLookupState, cs);
});
},

confirmDelete(event) { confirmDelete(event) {
if (confirm('Delete this job? This cannot be undone.')) { if (confirm('Delete this job? This cannot be undone.')) {
event.target.submit(); event.target.submit();


+ 6
- 0
routes/web.php Просмотреть файл

@@ -6,6 +6,7 @@ use App\Controllers\ApiProxyController;
use App\Controllers\AuthController; use App\Controllers\AuthController;
use App\Controllers\CampaignController; use App\Controllers\CampaignController;
use App\Controllers\CampaignTypeController; use App\Controllers\CampaignTypeController;
use App\Controllers\CustomerApiController;
use App\Controllers\CustomerController; use App\Controllers\CustomerController;
use App\Controllers\CustomerTypeController; use App\Controllers\CustomerTypeController;
use App\Controllers\HealthController; use App\Controllers\HealthController;
@@ -13,6 +14,11 @@ use App\Controllers\HomeController;
use App\Controllers\JobController; use App\Controllers\JobController;
use App\Controllers\JobTypeController; use App\Controllers\JobTypeController;


// ── Customer API (public JSON endpoints) ──────────────────────────────────────
$router->get('/api/customers', [CustomerApiController::class, 'customers']);
$router->get('/api/customers/{id}', [CustomerApiController::class, 'customer']);
$router->get('/api/customer-types', [CustomerApiController::class, 'customerTypes']);

// ── API Proxy ───────────────────────────────────────────────────────────────── // ── API Proxy ─────────────────────────────────────────────────────────────────
$router->get('/api/proxy', [ApiProxyController::class, 'fetch'])->middleware('auth'); $router->get('/api/proxy', [ApiProxyController::class, 'fetch'])->middleware('auth');




Загрузка…
Отмена
Сохранить

Powered by TurnKey Linux.