You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
17KB

  1. <script>
  2. window.__customerTypes = <?= json_encode($model->customerTypes, JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
  3. window.__initialCtId = <?= json_encode($model->form['customer_type_id'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
  4. window.__initialCtVals = <?= json_encode($model->form['attribute_values'], JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
  5. </script>
  6. <section class="content-stack" x-data="customerForm(window.__customerTypes, window.__initialCtId, window.__initialCtVals)">
  7. <div class="page-toolbar">
  8. <div class="section-heading">
  9. <h1><?= e($model->title) ?></h1>
  10. <p>Select a customer type, then fill in the attribute values.</p>
  11. </div>
  12. <a class="button button-secondary" href="/customers">&larr; Back to list</a>
  13. </div>
  14. <?php if (!$model->customerTypes): ?>
  15. <div class="alert alert-error">
  16. No customer types exist yet. <a href="/customer-types/create">Create a customer type</a> before adding customers.
  17. </div>
  18. <?php else: ?>
  19. <section class="section-panel">
  20. <?php if (isset($model->errors['_token'])): ?>
  21. <div class="alert alert-error"><?= e($model->errors['_token'][0]) ?></div>
  22. <?php endif; ?>
  23. <?php if (isset($model->errors['_duplicate'])): ?>
  24. <div class="alert alert-error"><?= $model->errors['_duplicate'][0] ?></div>
  25. <?php endif; ?>
  26. <form method="post" action="/customers" class="ct-form" novalidate>
  27. <?= csrf_field() ?>
  28. <div class="form-section">
  29. <label class="field field-full">
  30. <span>Customer type <span class="required-mark">*</span></span>
  31. <select class="input<?= isset($model->errors['customer_type_id']) ? ' input-error' : '' ?>"
  32. name="customer_type_id" x-model="selectedTypeId" x-on:change="onTypeChange()" required>
  33. <option value="0">— Select a customer type —</option>
  34. <?php foreach ($model->customerTypes as $ct): ?>
  35. <option value="<?= e((string) $ct['id']) ?>"
  36. <?= (int) $model->form['customer_type_id'] === $ct['id'] ? 'selected' : '' ?>>
  37. <?= e($ct['name']) ?>
  38. </option>
  39. <?php endforeach; ?>
  40. </select>
  41. <?php if (isset($model->errors['customer_type_id'])): ?>
  42. <small class="field-error"><?= e($model->errors['customer_type_id'][0]) ?></small>
  43. <?php endif; ?>
  44. </label>
  45. </div>
  46. <div class="form-section" x-show="currentAttributes.length > 0">
  47. <div class="attributes-header">
  48. <h3>Attribute values</h3>
  49. <p class="attributes-hint">Fields defined by the selected customer type.</p>
  50. </div>
  51. <div class="form-grid">
  52. <template x-for="attr in currentAttributes" :key="attr.name">
  53. <label class="field" :class="{ 'api-lookup-label': attr.type === 'api_lookup' }">
  54. <span x-text="attr.name"></span>
  55. <template x-if="attr.type === 'boolean'">
  56. <select class="input"
  57. :name="`attribute_values[${attr.name}]`"
  58. x-on:change="attributeValues[attr.name] = $event.target.value">
  59. <option value="" :selected="!attributeValues[attr.name]">— Select —</option>
  60. <option value="true" :selected="attributeValues[attr.name] === 'true'">True</option>
  61. <option value="false" :selected="attributeValues[attr.name] === 'false'">False</option>
  62. </select>
  63. </template>
  64. <template x-if="attr.type === 'api_lookup'">
  65. <div class="api-lookup-field">
  66. <input class="input" type="text"
  67. :name="`attribute_values[${attr.name}]`"
  68. x-model="attributeValues[attr.name]"
  69. placeholder="Type to search…"
  70. autocomplete="off"
  71. x-on:focus="openApiLookup(attr.name)"
  72. x-on:blur="closeApiLookup(attr.name)"
  73. x-on:keydown.escape.prevent="closeApiLookup(attr.name)">
  74. <div class="api-lookup-dropdown"
  75. x-show="apiLookupOpen[attr.name]"
  76. x-on:mousedown.prevent>
  77. <p class="api-lookup-loading" x-show="apiLookupState[attr.name] === 'loading'">Loading…</p>
  78. <div class="api-lookup-table" x-show="apiLookupState[attr.name] !== 'loading' && apiLookupState[attr.name] !== 'error'">
  79. <div class="api-lookup-thead"
  80. :style="`grid-template-columns: repeat(${getApiOptions(attr.name).fields.length || 1}, 1fr)`">
  81. <template x-for="f in getApiOptions(attr.name).fields" :key="f">
  82. <span x-text="f"></span>
  83. </template>
  84. </div>
  85. <div class="api-lookup-tbody">
  86. <template x-for="rec in getFilteredRecords(attr.name, attributeValues[attr.name])" :key="rec._primary">
  87. <div class="api-lookup-tr"
  88. :class="{ 'is-selected': attributeValues[attr.name] === rec._primary }"
  89. :style="`grid-template-columns: repeat(${getApiOptions(attr.name).fields.length || 1}, 1fr)`"
  90. x-on:click="selectApiOption(attr, rec)">
  91. <template x-for="(v, i) in rec._display" :key="i">
  92. <span x-text="v"></span>
  93. </template>
  94. </div>
  95. </template>
  96. <div class="api-lookup-empty" x-show="!getFilteredRecords(attr.name, attributeValues[attr.name]).length">No results.</div>
  97. </div>
  98. </div>
  99. <small class="field-error" x-show="apiLookupState[attr.name] === 'error'" x-text="apiLookupError[attr.name] || 'Fetch failed.'"></small>
  100. </div>
  101. </div>
  102. </template>
  103. <template x-if="attr.type !== 'boolean' && attr.type !== 'api_lookup'">
  104. <input class="input" :type="inputType(attr.type)"
  105. :name="`attribute_values[${attr.name}]`"
  106. :value="attributeValues[attr.name] ?? ''"
  107. x-on:input="attributeValues[attr.name] = $event.target.value">
  108. </template>
  109. </label>
  110. </template>
  111. </div>
  112. </div>
  113. <p class="attributes-hint" x-show="selectedTypeId && currentAttributes.length === 0">
  114. This customer type has no attributes defined.
  115. </p>
  116. <div class="form-actions">
  117. <button class="button button-primary" type="submit">Save Customer</button>
  118. <a class="button button-secondary" href="/customers">Cancel</a>
  119. </div>
  120. </form>
  121. </section>
  122. <!-- ── CSV Import Panel ─────────────────────────────────────────────── -->
  123. <section class="section-panel" x-show="selectedTypeId && selectedTypeId !== '0'" x-cloak>
  124. <div class="panel-header">
  125. <div>
  126. <h2>Import from CSV</h2>
  127. <p>Upload a CSV file, map columns to attributes, preview, then approve the import.</p>
  128. </div>
  129. </div>
  130. <!-- Step: idle — file picker -->
  131. <div x-show="csvStep === 'idle'">
  132. <div class="form-section" style="padding-top:12px">
  133. <label class="field field-full">
  134. <span>CSV file</span>
  135. <input class="input" type="file" accept=".csv"
  136. x-ref="csvFileInput"
  137. x-on:change="onCsvFileSelect($event)">
  138. </label>
  139. </div>
  140. <div class="form-actions import-actions">
  141. <button class="button button-secondary" type="button"
  142. :disabled="!csvFileSelected || isCsvUploading"
  143. x-on:click="uploadCsv()">
  144. Load CSV
  145. </button>
  146. <span class="inline-indicator" x-cloak x-show="isCsvUploading">Reading file…</span>
  147. </div>
  148. </div>
  149. <!-- Step: mapping — field column mapper -->
  150. <div x-cloak x-show="csvStep === 'mapping'">
  151. <p class="attributes-hint" style="margin:12px 0 8px">
  152. Auto-matched columns are shown in green. Use the dropdowns to assign any unmatched attributes.
  153. </p>
  154. <div class="import-mapping-wrap">
  155. <table class="import-mapping-table">
  156. <thead>
  157. <tr>
  158. <th>Attribute</th>
  159. <th>CSV Column</th>
  160. </tr>
  161. </thead>
  162. <tbody>
  163. <template x-if="currentAttributes.length === 0">
  164. <tr><td colspan="2" style="padding:12px;color:var(--text-muted)">This customer type has no attributes — nothing to map.</td></tr>
  165. </template>
  166. <template x-for="attr in currentAttributes" :key="attr.name">
  167. <tr>
  168. <td x-text="attr.name"></td>
  169. <td>
  170. <template x-if="csvMapping[attr.name]">
  171. <div class="mapping-matched">
  172. <span x-text="csvMapping[attr.name]"></span>
  173. <button type="button" class="mapping-clear" title="Remove mapping"
  174. x-on:click="clearMapping(attr.name)">&#215;</button>
  175. </div>
  176. </template>
  177. <template x-if="!csvMapping[attr.name]">
  178. <select class="input"
  179. x-on:change="setMapping(attr.name, $event.target.value)">
  180. <option value="">— Not mapped —</option>
  181. <template x-for="col in unusedCsvHeaders(attr.name)" :key="col">
  182. <option :value="col" x-text="col"></option>
  183. </template>
  184. </select>
  185. </template>
  186. </td>
  187. </tr>
  188. </template>
  189. </tbody>
  190. </table>
  191. </div>
  192. <div class="form-actions import-actions">
  193. <button class="button button-primary" type="button"
  194. :disabled="isCsvPreviewing"
  195. x-on:click="previewCsv()">
  196. Preview Import
  197. </button>
  198. <button class="button button-secondary" type="button"
  199. x-on:click="resetCsvImport()">
  200. Reset
  201. </button>
  202. <span class="inline-indicator" x-cloak x-show="isCsvPreviewing">Previewing…</span>
  203. </div>
  204. </div>
  205. <!-- Step: preview — review rows before insert -->
  206. <div x-cloak x-show="csvStep === 'preview'">
  207. <div class="import-preview-stats">
  208. <span class="preview-stat preview-stat-ok"
  209. x-show="csvPreviewStats.ok > 0">
  210. <span x-text="csvPreviewStats.ok"></span> will import
  211. </span>
  212. <span class="preview-stat preview-stat-duplicate"
  213. x-show="csvPreviewStats.duplicate > 0">
  214. <span x-text="csvPreviewStats.duplicate"></span> duplicate
  215. </span>
  216. <span class="preview-stat preview-stat-empty"
  217. x-show="csvPreviewStats.empty > 0">
  218. <span x-text="csvPreviewStats.empty"></span> empty (skipped)
  219. </span>
  220. </div>
  221. <div class="import-preview-scroll">
  222. <table class="import-preview-table">
  223. <thead>
  224. <tr>
  225. <th>#</th>
  226. <template x-for="attr in currentAttributes" :key="attr.name">
  227. <th x-text="attr.name"></th>
  228. </template>
  229. <th>Status</th>
  230. </tr>
  231. </thead>
  232. <tbody>
  233. <template x-for="row in csvPreviewRows" :key="row.index">
  234. <tr :class="'import-row-' + row.status">
  235. <td x-text="row.index"></td>
  236. <template x-for="attr in currentAttributes" :key="attr.name">
  237. <td x-text="row.values[attr.name] ?? ''"></td>
  238. </template>
  239. <td class="import-preview-status">
  240. <span :class="'import-badge import-badge-' + row.status"
  241. x-text="row.status === 'ok' ? 'OK' : row.status === 'duplicate' ? 'Duplicate' : 'Empty'"></span>
  242. <span class="import-row-msg" x-show="row.message" x-text="row.message || ''"></span>
  243. </td>
  244. </tr>
  245. </template>
  246. </tbody>
  247. </table>
  248. </div>
  249. <div class="form-actions import-actions">
  250. <button class="button button-primary" type="button"
  251. :disabled="isCsvApproving || (csvPreviewStats.ok === 0 && csvPreviewStats.duplicate === 0)"
  252. x-on:click="approveCsv()">
  253. Approve Import
  254. </button>
  255. <button class="button button-secondary" type="button"
  256. :disabled="isCsvApproving"
  257. x-on:click="csvStep = 'mapping'">
  258. &larr; Back to Mapping
  259. </button>
  260. <span class="inline-indicator" x-cloak x-show="isCsvApproving">Importing…</span>
  261. </div>
  262. </div>
  263. <!-- Step: done — summary -->
  264. <div x-cloak x-show="csvStep === 'done'" style="padding:12px 0">
  265. <div class="alert alert-success" x-text="csvDoneMessage"></div>
  266. <template x-if="csvApproveErrors.length > 0">
  267. <div class="alert alert-error" style="margin-top:8px">
  268. <strong>Some rows had errors:</strong>
  269. <ul style="margin:4px 0 0;padding-left:18px">
  270. <template x-for="(err, i) in csvApproveErrors" :key="i">
  271. <li x-text="err"></li>
  272. </template>
  273. </ul>
  274. </div>
  275. </template>
  276. <div class="form-actions import-actions">
  277. <a class="button button-primary" href="/customers">View Customers</a>
  278. <button class="button button-secondary" type="button"
  279. x-on:click="resetCsvImport()">
  280. Import Another File
  281. </button>
  282. </div>
  283. </div>
  284. <!-- Error bar (visible in all steps) -->
  285. <div class="alert alert-error" x-cloak x-show="csvError" x-text="csvError"
  286. style="margin-top:12px"></div>
  287. </section>
  288. <?php endif; ?>
  289. </section>

Powered by TurnKey Linux.