|
- <script>
- window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
- window.__initialCtId = <?= json_encode($model->form['customer_type_id'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
- window.__initialCtVals = <?= json_encode($model->form['attribute_values'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
- </script>
-
- <section class="content-stack" x-data="customerForm(window.__customerTypes, window.__initialCtId, window.__initialCtVals)">
-
- <div class="page-toolbar">
- <div class="section-heading">
- <h1><?= e($model->title) ?></h1>
- <p>Select a customer type, then fill in the attribute values.</p>
- </div>
- <a class="button button-secondary" href="/customers">← Back to list</a>
- </div>
-
- <?php if (!$model->customerTypes): ?>
- <div class="alert alert-error">
- No customer types exist yet. <a href="/customer-types/create">Create a customer type</a> before adding customers.
- </div>
- <?php else: ?>
-
- <section class="section-panel">
-
- <?php if (isset($model->errors['_token'])): ?>
- <div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
- <?php endif; ?>
-
- <?php if (isset($model->errors['_duplicate'])): ?>
- <div class="alert alert-error"><?= $model->errors['_duplicate'][0] ?></div>
- <?php endif; ?>
-
- <form method="post" action="/customers" class="ct-form" novalidate>
- <?= csrf_field() ?>
-
- <div class="form-section">
- <label class="field field-full">
- <span>Customer type <span class="required-mark">*</span></span>
- <select class="input<?= isset($model->errors['customer_type_id']) ? ' input-error' : '' ?>"
- name="customer_type_id" x-model="selectedTypeId" x-on:change="onTypeChange()" required>
- <option value="0">— Select a customer type —</option>
- <?php foreach ($model->customerTypes as $ct): ?>
- <option value="<?= e((string) $ct['id']) ?>"
- <?= (int) $model->form['customer_type_id'] === $ct['id'] ? 'selected' : '' ?>>
- <?= e($ct['name']) ?>
- </option>
- <?php endforeach; ?>
- </select>
- <?php if (isset($model->errors['customer_type_id'])): ?>
- <small class="field-error"><?= e($model->errors['customer_type_id'][0]) ?></small>
- <?php endif; ?>
- </label>
- </div>
-
- <div class="form-section" x-show="currentAttributes.length > 0">
- <div class="attributes-header">
- <h3>Attribute values</h3>
- <p class="attributes-hint">Fields defined by the selected customer type.</p>
- </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' }">
- <span x-text="attr.name"></span>
- <template x-if="attr.type === 'boolean'">
- <select class="input"
- :name="`attribute_values[${attr.name}]`"
- x-on:change="attributeValues[attr.name] = $event.target.value">
- <option value="" :selected="!attributeValues[attr.name]">— Select —</option>
- <option value="true" :selected="attributeValues[attr.name] === 'true'">True</option>
- <option value="false" :selected="attributeValues[attr.name] === 'false'">False</option>
- </select>
- </template>
- <template x-if="attr.type === 'api_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…"
- autocomplete="off"
- x-on:focus="openApiLookup(attr.name)"
- x-on:blur="closeApiLookup(attr.name)"
- x-on:keydown.escape.prevent="closeApiLookup(attr.name)">
- <div class="api-lookup-dropdown"
- x-show="apiLookupOpen[attr.name]"
- x-on:mousedown.prevent>
- <p class="api-lookup-loading" x-show="apiLookupState[attr.name] === 'loading'">Loading…</p>
- <div class="api-lookup-table" x-show="apiLookupState[attr.name] !== 'loading' && apiLookupState[attr.name] !== 'error'">
- <div class="api-lookup-thead"
- :style="`grid-template-columns: repeat(${getApiOptions(attr.name).fields.length || 1}, 1fr)`">
- <template x-for="f in getApiOptions(attr.name).fields" :key="f">
- <span x-text="f"></span>
- </template>
- </div>
- <div class="api-lookup-tbody">
- <template x-for="rec in getFilteredRecords(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(${getApiOptions(attr.name).fields.length || 1}, 1fr)`"
- x-on:click="selectApiOption(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="!getFilteredRecords(attr.name, attributeValues[attr.name]).length">No results.</div>
- </div>
- </div>
- <small class="field-error" x-show="apiLookupState[attr.name] === 'error'" x-text="apiLookupError[attr.name] || 'Fetch failed.'"></small>
- </div>
- </div>
- </template>
- <template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
- <input class="input" :type="inputType(attr.type)"
- :name="`attribute_values[${attr.name}]`"
- :value="attributeValues[attr.name] ?? ''"
- x-on:input="attributeValues[attr.name] = $event.target.value">
- </template>
- </label>
- </template>
- </div>
- </div>
-
- <p class="attributes-hint" x-show="selectedTypeId && currentAttributes.length === 0">
- This customer type has no attributes defined.
- </p>
-
- <div class="form-actions">
- <button class="button button-primary" type="submit">Save Customer</button>
- <a class="button button-secondary" href="/customers">Cancel</a>
- </div>
- </form>
-
- </section>
-
- <!-- ── CSV Import Panel ─────────────────────────────────────────────── -->
-
- <section class="section-panel" x-show="selectedTypeId && selectedTypeId !== '0'" x-cloak>
- <div class="panel-header">
- <div>
- <h2>Import from CSV</h2>
- <p>Upload a CSV file, map columns to attributes, preview, then approve the import.</p>
- </div>
- </div>
-
- <!-- Step: idle — file picker -->
- <div x-show="csvStep === 'idle'">
- <div class="form-section" style="padding-top:12px">
- <label class="field field-full">
- <span>CSV file</span>
- <input class="input" type="file" accept=".csv"
- x-ref="csvFileInput"
- x-on:change="onCsvFileSelect($event)">
- </label>
- </div>
- <div class="form-actions import-actions">
- <button class="button button-secondary" type="button"
- :disabled="!csvFileSelected || isCsvUploading"
- x-on:click="uploadCsv()">
- Load CSV
- </button>
- <span class="inline-indicator" x-cloak x-show="isCsvUploading">Reading file…</span>
- </div>
- </div>
-
- <!-- Step: mapping — field column mapper -->
- <div x-cloak x-show="csvStep === 'mapping'">
- <p class="attributes-hint" style="margin:12px 0 8px">
- Auto-matched columns are shown in green. Use the dropdowns to assign any unmatched attributes.
- </p>
- <div class="import-mapping-wrap">
- <table class="import-mapping-table">
- <thead>
- <tr>
- <th>Attribute</th>
- <th>CSV Column</th>
- </tr>
- </thead>
- <tbody>
- <template x-if="currentAttributes.length === 0">
- <tr><td colspan="2" style="padding:12px;color:var(--text-muted)">This customer type has no attributes — nothing to map.</td></tr>
- </template>
- <template x-for="attr in currentAttributes" :key="attr.name">
- <tr>
- <td x-text="attr.name"></td>
- <td>
- <template x-if="csvMapping[attr.name]">
- <div class="mapping-matched">
- <span x-text="csvMapping[attr.name]"></span>
- <button type="button" class="mapping-clear" title="Remove mapping"
- x-on:click="clearMapping(attr.name)">×</button>
- </div>
- </template>
- <template x-if="!csvMapping[attr.name]">
- <select class="input"
- x-on:change="setMapping(attr.name, $event.target.value)">
- <option value="">— Not mapped —</option>
- <template x-for="col in unusedCsvHeaders(attr.name)" :key="col">
- <option :value="col" x-text="col"></option>
- </template>
- </select>
- </template>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- <div class="form-actions import-actions">
- <button class="button button-primary" type="button"
- :disabled="isCsvPreviewing"
- x-on:click="previewCsv()">
- Preview Import
- </button>
- <button class="button button-secondary" type="button"
- x-on:click="resetCsvImport()">
- Reset
- </button>
- <span class="inline-indicator" x-cloak x-show="isCsvPreviewing">Previewing…</span>
- </div>
- </div>
-
- <!-- Step: preview — review rows before insert -->
- <div x-cloak x-show="csvStep === 'preview'">
- <div class="import-preview-stats">
- <span class="preview-stat preview-stat-ok"
- x-show="csvPreviewStats.ok > 0">
- <span x-text="csvPreviewStats.ok"></span> will import
- </span>
- <span class="preview-stat preview-stat-duplicate"
- x-show="csvPreviewStats.duplicate > 0">
- <span x-text="csvPreviewStats.duplicate"></span> duplicate
- </span>
- <span class="preview-stat preview-stat-empty"
- x-show="csvPreviewStats.empty > 0">
- <span x-text="csvPreviewStats.empty"></span> empty (skipped)
- </span>
- </div>
-
- <div class="import-preview-scroll">
- <table class="import-preview-table">
- <thead>
- <tr>
- <th>#</th>
- <template x-for="attr in currentAttributes" :key="attr.name">
- <th x-text="attr.name"></th>
- </template>
- <th>Status</th>
- </tr>
- </thead>
- <tbody>
- <template x-for="row in csvPreviewRows" :key="row.index">
- <tr :class="'import-row-' + row.status">
- <td x-text="row.index"></td>
- <template x-for="attr in currentAttributes" :key="attr.name">
- <td x-text="row.values[attr.name] ?? ''"></td>
- </template>
- <td class="import-preview-status">
- <span :class="'import-badge import-badge-' + row.status"
- x-text="row.status === 'ok' ? 'OK' : row.status === 'duplicate' ? 'Duplicate' : 'Empty'"></span>
- <span class="import-row-msg" x-show="row.message" x-text="row.message || ''"></span>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
-
- <div class="form-actions import-actions">
- <button class="button button-primary" type="button"
- :disabled="isCsvApproving || (csvPreviewStats.ok === 0 && csvPreviewStats.duplicate === 0)"
- x-on:click="approveCsv()">
- Approve Import
- </button>
- <button class="button button-secondary" type="button"
- :disabled="isCsvApproving"
- x-on:click="csvStep = 'mapping'">
- ← Back to Mapping
- </button>
- <span class="inline-indicator" x-cloak x-show="isCsvApproving">Importing…</span>
- </div>
- </div>
-
- <!-- Step: done — summary -->
- <div x-cloak x-show="csvStep === 'done'" style="padding:12px 0">
- <div class="alert alert-success" x-text="csvDoneMessage"></div>
- <template x-if="csvApproveErrors.length > 0">
- <div class="alert alert-error" style="margin-top:8px">
- <strong>Some rows had errors:</strong>
- <ul style="margin:4px 0 0;padding-left:18px">
- <template x-for="(err, i) in csvApproveErrors" :key="i">
- <li x-text="err"></li>
- </template>
- </ul>
- </div>
- </template>
- <div class="form-actions import-actions">
- <a class="button button-primary" href="/customers">View Customers</a>
- <button class="button button-secondary" type="button"
- x-on:click="resetCsvImport()">
- Import Another File
- </button>
- </div>
- </div>
-
- <!-- Error bar (visible in all steps) -->
- <div class="alert alert-error" x-cloak x-show="csvError" x-text="csvError"
- style="margin-top:12px"></div>
-
- </section>
-
- <?php endif; ?>
-
- </section>
|