Browse Source

add customer type attribute import to job type editor

Job type attributes now support a "Customer Type" pseudo-type: selecting
a customer type expands its attribute definitions inline as real rows,
letting job types reuse field configurations without manual re-entry.
The "customer" placeholder type is stripped server-side before persisting.

Also update production Keycloak redirect URIs from IP to hostname and
ignore .claude/ and .abacusai/ tool directories.
Customer_Type_Lookup
Daniel Covington 2 weeks ago
parent
commit
2d707431b0
7 changed files with 130 additions and 19 deletions
  1. +2
    -2
      .env_prod
  2. +3
    -0
      .gitignore
  3. +37
    -11
      app/Controllers/JobTypeController.php
  4. +3
    -0
      app/ViewModels/JobTypeViewModel.php
  5. +22
    -2
      app/Views/job-types/create.php
  6. +22
    -2
      app/Views/job-types/edit.php
  7. +41
    -2
      public/js/app.js

+ 2
- 2
.env_prod View File

@@ -15,5 +15,5 @@ KEYCLOAK_BASE_URL=http://kci-app01.ntp.kentcommunications.com:8180/
KEYCLOAK_REALM=KCI KEYCLOAK_REALM=KCI
KEYCLOAK_CLIENT_ID=canopy-web KEYCLOAK_CLIENT_ID=canopy-web
KEYCLOAK_CLIENT_SECRET=LHWXp5UUuES00Dz2iCjTJJgX9su6co0y KEYCLOAK_CLIENT_SECRET=LHWXp5UUuES00Dz2iCjTJJgX9su6co0y
KEYCLOAK_REDIRECT_URI=http://192.168.1.200:8801/auth/callback
KEYCLOAK_LOGOUT_REDIRECT_URI=http://192.168.1.200:8801/login
KEYCLOAK_REDIRECT_URI=http://ct.ntp.kentcommunications.com:8801/auth/callback
KEYCLOAK_LOGOUT_REDIRECT_URI=http:ct.ntp.kentcommunications.com:8801/login

+ 3
- 0
.gitignore View File

@@ -45,3 +45,6 @@ desktop.ini
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
yarn-error.log* yarn-error.log*

.claude/
.abacusai/

+ 37
- 11
app/Controllers/JobTypeController.php View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;


use App\Models\JobType; use App\Models\JobType;
use App\Repositories\CustomerTypeRepository;
use App\Repositories\JobTypeAuditRepository; use App\Repositories\JobTypeAuditRepository;
use App\Repositories\JobTypeRepository; use App\Repositories\JobTypeRepository;
use App\ViewModels\JobTypeViewModel; use App\ViewModels\JobTypeViewModel;
@@ -52,7 +53,8 @@ class JobTypeController extends Controller
public function create(): Response public function create(): Response
{ {
$model = new JobTypeViewModel(); $model = new JobTypeViewModel();
$model->title = 'New Job Type';
$model->title = 'New Job Type';
$model->customerTypes = $this->loadCustomerTypes();


return $this->view('job-types.create', [ return $this->view('job-types.create', [
'model' => $model, 'model' => $model,
@@ -71,9 +73,10 @@ class JobTypeController extends Controller


if (!empty($errors)) { if (!empty($errors)) {
$model = new JobTypeViewModel(); $model = new JobTypeViewModel();
$model->title = 'New Job Type';
$model->form = $form;
$model->errors = $errors;
$model->title = 'New Job Type';
$model->form = $form;
$model->errors = $errors;
$model->customerTypes = $this->loadCustomerTypes();


return $this->view('job-types.create', [ return $this->view('job-types.create', [
'model' => $model, 'model' => $model,
@@ -104,9 +107,10 @@ class JobTypeController extends Controller
} }


$model = new JobTypeViewModel(); $model = new JobTypeViewModel();
$model->title = 'Edit Job Type';
$model->jobType = $row;
$model->saved = Request::capture()->input('saved') === '1';
$model->title = 'Edit Job Type';
$model->jobType = $row;
$model->saved = Request::capture()->input('saved') === '1';
$model->customerTypes = $this->loadCustomerTypes();
$model->form = [ $model->form = [
'name' => (string) $row['name'], 'name' => (string) $row['name'],
'attributes' => json_decode((string) ($row['attributes'] ?? '[]'), true) ?? [], 'attributes' => json_decode((string) ($row['attributes'] ?? '[]'), true) ?? [],
@@ -138,10 +142,11 @@ class JobTypeController extends Controller


if (!empty($errors)) { if (!empty($errors)) {
$model = new JobTypeViewModel(); $model = new JobTypeViewModel();
$model->title = 'Edit Job Type';
$model->jobType = $row;
$model->form = $form;
$model->errors = $errors;
$model->title = 'Edit Job Type';
$model->jobType = $row;
$model->form = $form;
$model->errors = $errors;
$model->customerTypes = $this->loadCustomerTypes();


return $this->view('job-types.edit', [ return $this->view('job-types.edit', [
'model' => $model, 'model' => $model,
@@ -232,6 +237,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.
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'], true) ? $attrType : 'text';


$attr = [ $attr = [
@@ -274,4 +283,21 @@ class JobTypeController extends Controller
{ {
return new JobTypeAuditRepository(database()); return new JobTypeAuditRepository(database());
} }

private function customerTypeRepo(): CustomerTypeRepository
{
return new CustomerTypeRepository(database());
}

/** @return list<array{id: int, name: 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) ?? [],
];
}, $this->customerTypeRepo()->allOrderedByName());
}
} }

+ 3
- 0
app/ViewModels/JobTypeViewModel.php View File

@@ -20,4 +20,7 @@ class JobTypeViewModel


/** @var array<string, mixed>|null */ /** @var array<string, mixed>|null */
public ?array $jobType = null; public ?array $jobType = null;

/** @var list<array{id: int, name: string, attributes: array}> */
public array $customerTypes = [];
} }

+ 22
- 2
app/Views/job-types/create.php View File

@@ -1,4 +1,7 @@
<script>window.__jtAttributes = <?= json_encode($model->form['attributes'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;</script>
<script>
window.__jtAttributes = <?= json_encode($model->form['attributes'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
</script>


<section class="content-stack"> <section class="content-stack">


@@ -10,7 +13,7 @@
<a class="button button-secondary" href="/job-types">&larr; Back to list</a> <a class="button button-secondary" href="/job-types">&larr; Back to list</a>
</div> </div>


<section class="section-panel" x-data="jobTypeForm(window.__jtAttributes)">
<section class="section-panel" x-data="jobTypeForm(window.__jtAttributes, window.__customerTypes)">


<?php if (isset($model->errors['_token'])): ?> <?php if (isset($model->errors['_token'])): ?>
<div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div> <div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
@@ -71,6 +74,7 @@
<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>
</select> </select>
</label> </label>
<div class="attribute-remove"> <div class="attribute-remove">
@@ -117,6 +121,22 @@
</select> </select>
</label> </label>
</div> </div>
<div class="customer-type-config" x-show="attr.type === 'customer'">
<label class="field">
<span>Customer Type</span>
<select class="input"
x-model.number="attr.customer_type_id"
x-on:change="importCustomerTypeAttributes(index)">
<option :value="0">— Select a customer type —</option>
<template x-for="ct in customerTypes" :key="ct.id">
<option :value="ct.id" x-text="ct.name"></option>
</template>
</select>
</label>
<p class="attributes-hint" x-show="customerTypes.length === 0">
No customer types exist yet. <a href="/customer-types/create">Create one</a> first.
</p>
</div>
</div> </div>
</template> </template>
</div> </div>


+ 22
- 2
app/Views/job-types/edit.php View File

@@ -1,5 +1,8 @@
<?php $jobTypeId = (int) ($model->jobType['id'] ?? 0); ?> <?php $jobTypeId = (int) ($model->jobType['id'] ?? 0); ?>
<script>window.__jtAttributes = <?= json_encode($model->form['attributes'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;</script>
<script>
window.__jtAttributes = <?= json_encode($model->form['attributes'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
</script>


<section class="content-stack"> <section class="content-stack">


@@ -17,7 +20,7 @@
</div> </div>
<?php endif; ?> <?php endif; ?>


<section class="section-panel" x-data="jobTypeForm(window.__jtAttributes)">
<section class="section-panel" x-data="jobTypeForm(window.__jtAttributes, window.__customerTypes)">


<?php if (isset($model->errors['_token'])): ?> <?php if (isset($model->errors['_token'])): ?>
<div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div> <div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
@@ -78,6 +81,7 @@
<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>
</select> </select>
</label> </label>
<div class="attribute-remove"> <div class="attribute-remove">
@@ -124,6 +128,22 @@
</select> </select>
</label> </label>
</div> </div>
<div class="customer-type-config" x-show="attr.type === 'customer'">
<label class="field">
<span>Customer Type</span>
<select class="input"
x-model.number="attr.customer_type_id"
x-on:change="importCustomerTypeAttributes(index)">
<option :value="0">— Select a customer type —</option>
<template x-for="ct in customerTypes" :key="ct.id">
<option :value="ct.id" x-text="ct.name"></option>
</template>
</select>
</label>
<p class="attributes-hint" x-show="customerTypes.length === 0">
No customer types exist yet. <a href="/customer-types/create">Create one</a> first.
</p>
</div>
</div> </div>
</template> </template>
</div> </div>


+ 41
- 2
public/js/app.js View File

@@ -1171,14 +1171,53 @@ window.deleteJobType = function (id) {
_postDelete('/job-types/' + id + '/delete'); _postDelete('/job-types/' + id + '/delete');
}; };


window.jobTypeForm = function (initialAttributes) {
window.jobTypeForm = function (initialAttributes, customerTypes) {
return { return {
attributes: Array.isArray(initialAttributes) ? initialAttributes : [], attributes: Array.isArray(initialAttributes) ? initialAttributes : [],
customerTypes: Array.isArray(customerTypes) ? customerTypes : [],
dragIndex: null, dragIndex: null,
dragOverIndex: null, dragOverIndex: null,


addAttribute() { 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',
customer_type_id: 0,
});
},

importCustomerTypeAttributes(index) {
var row = this.attributes[index];
if (!row) return;

var ctId = Number(row.customer_type_id || 0);
if (!ctId) return;

var ct = this.customerTypes.find(function (c) { return Number(c.id) === ctId; });
if (!ct || !Array.isArray(ct.attributes) || ct.attributes.length === 0) {
row.customer_type_id = 0;
return;
}

var imported = ct.attributes.slice().sort(function (a, b) {
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,
};
});

this.attributes.splice.apply(this.attributes, [index, 1].concat(imported));
this.renumberOrder();
}, },


removeAttribute(index) { removeAttribute(index) {


Loading…
Cancel
Save

Powered by TurnKey Linux.