customer_api_test in i main 2 veckor sedan
| @@ -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, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -223,12 +223,13 @@ 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') ?? []); | |||
| $attributeCustomerTypeIds = (array) ($request->input('attribute_customer_type_id') ?? []); | |||
| $attributes = []; | |||
| @@ -237,11 +238,10 @@ class JobTypeController extends Controller | |||
| $attrType = trim((string) ($attributeTypes[$i] ?? 'text')); | |||
| 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; | |||
| $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 = [ | |||
| 'name' => $attrName, | |||
| @@ -262,6 +262,11 @@ class JobTypeController extends Controller | |||
| $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; | |||
| } | |||
| @@ -289,14 +294,15 @@ class JobTypeController extends Controller | |||
| 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 | |||
| { | |||
| return array_map(static function (array $t): array { | |||
| 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()); | |||
| } | |||
| @@ -48,20 +48,39 @@ class CustomerRepository extends Repository | |||
| '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.attributes AS customer_type_attributes, | |||
| ct.api_match_field | |||
| FROM customer c | |||
| INNER JOIN customer_type ct ON c.customer_type_id = ct.id | |||
| 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 | |||
| { | |||
| return $this->database->first( | |||
| '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.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.id = :id', | |||
| @@ -74,7 +74,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T | |||
| <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"> | |||
| @@ -121,7 +121,9 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T | |||
| </select> | |||
| </label> | |||
| </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"> | |||
| <span>Customer Type</span> | |||
| <select class="input" | |||
| @@ -81,7 +81,7 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T | |||
| <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"> | |||
| @@ -128,7 +128,9 @@ window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_T | |||
| </select> | |||
| </label> | |||
| </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"> | |||
| <span>Customer Type</span> | |||
| <select class="input" | |||
| @@ -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" | |||
| @@ -126,7 +126,45 @@ 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 === '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)" | |||
| :name="`attribute_values[${attr.name}]`" | |||
| :value="attributeValues[attr.name] ?? ''" | |||
| @@ -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" | |||
| @@ -123,7 +123,45 @@ 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 === '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)" | |||
| :name="`attribute_values[${attr.name}]`" | |||
| :value="attributeValues[attr.name] ?? ''" | |||
| @@ -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" | |||
| ); | |||
| } | |||
| }; | |||
| @@ -1202,21 +1202,29 @@ window.jobTypeForm = function (initialAttributes, customerTypes) { | |||
| var imported = ct.attributes.slice().sort(function (a, b) { | |||
| return (a.order || 0) - (b.order || 0); | |||
| }).map(function (a) { | |||
| var importedType = (a.type === 'api_lookup') ? 'text' : (a.type || 'text'); | |||
| 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, | |||
| }; | |||
| }); | |||
| 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(); | |||
| }, | |||
| @@ -1441,10 +1449,13 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) { | |||
| jobTypes: jobTypes, | |||
| selectedTypeId: String(initialTypeId || ''), | |||
| attributeValues: Object.assign({}, initialValues || {}), | |||
| apiLookupState: {}, | |||
| apiLookupError: {}, | |||
| apiLookupOptions: {}, | |||
| apiLookupOpen: {}, | |||
| apiLookupState: {}, | |||
| apiLookupError: {}, | |||
| apiLookupOptions: {}, | |||
| apiLookupOpen: {}, | |||
| customerLookupState: {}, | |||
| customerLookupOptions: {}, | |||
| customerLookupOpen: {}, | |||
| get currentType() { | |||
| var id = this.selectedTypeId; | |||
| @@ -1462,19 +1473,24 @@ window.jobForm = function (jobTypes, initialTypeId, initialValues) { | |||
| 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.fetchCustomers(attr); } | |||
| }); | |||
| }, | |||
| 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; | |||
| 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.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) { | |||
| if (confirm('Delete this job? This cannot be undone.')) { | |||
| event.target.submit(); | |||
| @@ -6,6 +6,7 @@ use App\Controllers\ApiProxyController; | |||
| use App\Controllers\AuthController; | |||
| use App\Controllers\CampaignController; | |||
| use App\Controllers\CampaignTypeController; | |||
| use App\Controllers\CustomerApiController; | |||
| use App\Controllers\CustomerController; | |||
| use App\Controllers\CustomerTypeController; | |||
| use App\Controllers\HealthController; | |||
| @@ -13,6 +14,11 @@ use App\Controllers\HomeController; | |||
| use App\Controllers\JobController; | |||
| 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 ───────────────────────────────────────────────────────────────── | |||
| $router->get('/api/proxy', [ApiProxyController::class, 'fetch'])->middleware('auth'); | |||
Powered by TurnKey Linux.