2026-01-19 06:37:37 +07:00
|
|
|
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
2026-01-21 13:41:37 +07:00
|
|
|
<div class="modal-box p-5 border border-base-300 shadow-2xl bg-base-100 max-w-sm">
|
|
|
|
|
<h3 class="font-bold text-base mb-3 flex items-center gap-2 text-base-content">
|
|
|
|
|
<i class="fa-solid fa-vial text-primary text-sm"></i>
|
2026-01-19 06:37:37 +07:00
|
|
|
<span x-text="form.controlId ? 'Edit Control' : 'New Control'"></span>
|
|
|
|
|
</h3>
|
2026-01-21 13:41:37 +07:00
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
2026-02-05 20:06:51 +07:00
|
|
|
<template x-if="loadingDepartments">
|
2026-02-03 16:55:13 +07:00
|
|
|
<div class="flex items-center justify-center py-2">
|
|
|
|
|
<span class="loading loading-spinner loading-sm text-primary"></span>
|
|
|
|
|
<span class="ml-2 text-xs opacity-50">Loading departments...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="departments">
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text-alt font-semibold text-base-content/70">Department</span>
|
|
|
|
|
</label>
|
|
|
|
|
<select
|
|
|
|
|
class="select select-bordered select-sm w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content"
|
|
|
|
|
:class="{'border-error': errors.deptId}"
|
|
|
|
|
x-model="form.deptId"
|
|
|
|
|
placeholder="Select department">
|
|
|
|
|
<option value="">Select Department</option>
|
|
|
|
|
<template x-for="dept in departments" :key="dept.deptId">
|
|
|
|
|
<option :value="dept.deptId" x-text="dept.deptName"></option>
|
|
|
|
|
</template>
|
|
|
|
|
</select>
|
|
|
|
|
<template x-if="errors.deptId">
|
|
|
|
|
<label class="label">
|
|
|
|
|
<span class="label-text-alt text-error" x-text="errors.deptId"></span>
|
|
|
|
|
</label>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2026-02-12 11:32:04 +07:00
|
|
|
<div class="form-control relative" x-data="{ showAutocomplete: false, filteredNames: [], highlightIndex: -1 }" @click.outside="showAutocomplete = false">
|
2026-01-21 13:41:37 +07:00
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text-alt font-semibold text-base-content/70">Control Name</span>
|
2026-01-19 06:37:37 +07:00
|
|
|
</label>
|
2026-01-21 13:41:37 +07:00
|
|
|
<input type="text"
|
|
|
|
|
class="input input-bordered input-sm w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
|
2026-02-12 11:32:04 +07:00
|
|
|
:class="{'border-error': errors.controlName}"
|
|
|
|
|
x-model="form.controlName"
|
|
|
|
|
@input="filteredNames = uniqueControlNames.filter(n => n.toLowerCase().includes(form.controlName.toLowerCase())).slice(0, 10); showAutocomplete = filteredNames.length > 0 && form.controlName.length > 0; highlightIndex = -1"
|
|
|
|
|
@focus="filteredNames = uniqueControlNames.filter(n => n.toLowerCase().includes(form.controlName.toLowerCase())).slice(0, 10); showAutocomplete = filteredNames.length > 0 && form.controlName.length > 0"
|
|
|
|
|
@keydown.down.prevent="highlightIndex = Math.min(highlightIndex + 1, filteredNames.length - 1)"
|
|
|
|
|
@keydown.up.prevent="highlightIndex = Math.max(highlightIndex - 1, -1)"
|
|
|
|
|
@keydown.enter.prevent="if (highlightIndex >= 0) { form.controlName = filteredNames[highlightIndex]; showAutocomplete = false; }"
|
|
|
|
|
@keydown.escape="showAutocomplete = false"
|
2026-01-21 13:41:37 +07:00
|
|
|
placeholder="Enter control name" />
|
2026-02-12 11:32:04 +07:00
|
|
|
<div x-show="showAutocomplete"
|
|
|
|
|
x-transition:enter="transition ease-out duration-100"
|
|
|
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
|
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
|
|
|
class="absolute top-full left-0 right-0 mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg z-50 max-h-48 overflow-y-auto">
|
|
|
|
|
<template x-for="(name, index) in filteredNames" :key="name">
|
|
|
|
|
<div @click="form.controlName = name; showAutocomplete = false"
|
|
|
|
|
:class="{'bg-primary text-primary-content': index === highlightIndex, 'hover:bg-base-200': index !== highlightIndex}"
|
|
|
|
|
class="px-3 py-2 cursor-pointer text-sm"
|
|
|
|
|
x-text="name"></div>
|
2026-01-21 13:41:37 +07:00
|
|
|
</template>
|
2026-02-12 11:32:04 +07:00
|
|
|
</div>
|
2026-01-19 06:37:37 +07:00
|
|
|
<template x-if="errors.controlName">
|
|
|
|
|
<label class="label">
|
|
|
|
|
<span class="label-text-alt text-error" x-text="errors.controlName"></span>
|
|
|
|
|
</label>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
2026-01-21 13:41:37 +07:00
|
|
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
2026-01-19 06:37:37 +07:00
|
|
|
<div class="form-control">
|
2026-01-21 13:41:37 +07:00
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text-alt font-semibold text-base-content/70">Lot Number</span>
|
2026-01-19 06:37:37 +07:00
|
|
|
</label>
|
2026-01-21 13:41:37 +07:00
|
|
|
<input type="text"
|
|
|
|
|
class="input input-bordered input-sm w-full focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
|
|
|
|
|
x-model="form.lot" placeholder="e.g., LOT12345" />
|
2026-01-19 06:37:37 +07:00
|
|
|
</div>
|
2026-01-21 13:41:37 +07:00
|
|
|
|
2026-01-19 06:37:37 +07:00
|
|
|
<div class="form-control">
|
2026-01-21 13:41:37 +07:00
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text-alt font-semibold text-base-content/70">Expiry Date</span>
|
2026-01-19 06:37:37 +07:00
|
|
|
</label>
|
2026-01-21 13:41:37 +07:00
|
|
|
<input type="date"
|
|
|
|
|
class="input input-bordered input-sm w-full focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary bg-base-200 border-base-300 text-base-content"
|
|
|
|
|
x-model="form.expDate" />
|
2026-01-19 06:37:37 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-21 13:41:37 +07:00
|
|
|
|
2026-02-12 11:32:04 +07:00
|
|
|
<div class="form-control relative" x-data="{ showProdAutocomplete: false, filteredProducers: [], prodHighlightIndex: -1 }" @click.outside="showProdAutocomplete = false">
|
2026-01-21 13:41:37 +07:00
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text-alt font-semibold text-base-content/70">Producer</span>
|
2026-01-19 06:37:37 +07:00
|
|
|
</label>
|
2026-01-21 13:41:37 +07:00
|
|
|
<input type="text"
|
|
|
|
|
class="input input-bordered input-sm w-full focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
|
2026-03-04 09:21:41 +07:00
|
|
|
x-model="form.producer"
|
2026-02-12 11:32:04 +07:00
|
|
|
@input="filteredProducers = uniqueProducers.filter(p => p && p.toLowerCase().includes(form.producer.toLowerCase())).slice(0, 10); showProdAutocomplete = filteredProducers.length > 0 && form.producer.length > 0; prodHighlightIndex = -1"
|
|
|
|
|
@focus="filteredProducers = uniqueProducers.filter(p => p && p.toLowerCase().includes(form.producer.toLowerCase())).slice(0, 10); showProdAutocomplete = filteredProducers.length > 0 && form.producer.length > 0"
|
|
|
|
|
@keydown.down.prevent="prodHighlightIndex = Math.min(prodHighlightIndex + 1, filteredProducers.length - 1)"
|
|
|
|
|
@keydown.up.prevent="prodHighlightIndex = Math.max(prodHighlightIndex - 1, -1)"
|
|
|
|
|
@keydown.enter.prevent="if (prodHighlightIndex >= 0) { form.producer = filteredProducers[prodHighlightIndex]; showProdAutocomplete = false; }"
|
|
|
|
|
@keydown.escape="showProdAutocomplete = false"
|
|
|
|
|
placeholder="Enter producer name" />
|
2026-03-04 09:21:41 +07:00
|
|
|
<div x-show="showProdAutocomplete"
|
2026-02-12 11:32:04 +07:00
|
|
|
x-transition:enter="transition ease-out duration-100"
|
|
|
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
|
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
|
|
|
class="absolute top-full left-0 right-0 mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg z-50 max-h-48 overflow-y-auto">
|
|
|
|
|
<template x-for="(producer, index) in filteredProducers" :key="producer">
|
|
|
|
|
<div @click="form.producer = producer; showProdAutocomplete = false"
|
|
|
|
|
:class="{'bg-primary text-primary-content': index === prodHighlightIndex, 'hover:bg-base-200': index !== prodHighlightIndex}"
|
|
|
|
|
class="px-3 py-2 cursor-pointer text-sm"
|
|
|
|
|
x-text="producer"></div>
|
2026-01-21 13:41:37 +07:00
|
|
|
</template>
|
2026-02-12 11:32:04 +07:00
|
|
|
</div>
|
2026-01-19 06:37:37 +07:00
|
|
|
</div>
|
2026-03-04 09:21:41 +07:00
|
|
|
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label cursor-pointer justify-start gap-3 py-1">
|
|
|
|
|
<input type="checkbox"
|
|
|
|
|
class="checkbox checkbox-sm checkbox-primary"
|
|
|
|
|
x-model="form.isActive"
|
|
|
|
|
:checked="form.isActive == 1"
|
|
|
|
|
:value="1" />
|
|
|
|
|
<span class="label-text text-sm font-semibold text-base-content/70">Active</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
2026-01-19 06:37:37 +07:00
|
|
|
</div>
|
2026-01-21 13:41:37 +07:00
|
|
|
|
|
|
|
|
<div class="modal-action mt-5">
|
|
|
|
|
<button class="btn btn-sm btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
|
|
|
|
<button class="btn btn-sm btn-primary gap-2 shadow-md shadow-primary/20 font-medium"
|
|
|
|
|
:class="{'loading': loading}" @click="save()" :disabled="loading">
|
2026-01-19 06:37:37 +07:00
|
|
|
<template x-if="!loading">
|
|
|
|
|
<span><i class="fa-solid fa-save"></i> Save</span>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="loading">
|
|
|
|
|
<span>Saving...</span>
|
|
|
|
|
</template>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
2026-01-21 13:41:37 +07:00
|
|
|
</dialog>
|