Add focus-within input patterns, update Sidebar, TestMapModal, and PatientSearchBar components

This commit is contained in:
mahdahar 2026-02-25 16:38:28 +07:00
parent ad1618efec
commit ecc4822a38
5 changed files with 387 additions and 212 deletions

View File

@ -194,6 +194,39 @@ try {
- **Sizes**: `btn-sm`, `input-sm`, `select-sm` for compact forms - **Sizes**: `btn-sm`, `input-sm`, `select-sm` for compact forms
- **Custom**: `.compact-y`, `.compact-p`, `.compact-input`, `.compact-btn`, `.compact-card` - **Custom**: `.compact-y`, `.compact-p`, `.compact-input`, `.compact-btn`, `.compact-card`
### Input with Icons (DaisyUI Pattern)
**Correct way** - Use DaisyUI's built-in input with icon support:
```svelte
<!-- WRONG: Absolute positioning (icons may not render) -->
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2">
<User class="w-4 h-4" />
</span>
<input class="input input-sm input-bordered pl-9" />
</div>
<!-- CORRECT: DaisyUI input with flex layout -->
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<User class="w-4 h-4 text-gray-400" />
<input
type="text"
class="grow bg-transparent outline-none"
placeholder="Enter value..."
bind:value={someValue}
/>
</label>
```
**Key points:**
- Use `<label>` element as the input container with `input input-bordered` classes
- Add `flex items-center gap-2` for layout
- Icon is a **sibling** of the input, not absolutely positioned
- Input uses `class="grow bg-transparent outline-none"` to fill space
- Use `focus-within:input-primary` for focus state on the container
- Always add `text-gray-400` or similar color to the icon for visibility
## Project Structure ## Project Structure
``` ```

View File

@ -34,8 +34,10 @@ import {
// Collapsible section states - default collapsed // Collapsible section states - default collapsed
let laboratoryExpanded = $state(false); let laboratoryExpanded = $state(false);
let masterDataExpanded = $state(false);
let organizationExpanded = $state(false); let organizationExpanded = $state(false);
let labSetupExpanded = $state(false);
let usersContactsExpanded = $state(false);
let referenceDataExpanded = $state(false);
// Load states from localStorage on mount // Load states from localStorage on mount
$effect(() => { $effect(() => {
@ -45,8 +47,10 @@ import {
try { try {
const parsed = JSON.parse(savedStates); const parsed = JSON.parse(savedStates);
laboratoryExpanded = parsed.laboratory ?? false; laboratoryExpanded = parsed.laboratory ?? false;
masterDataExpanded = parsed.masterData ?? false;
organizationExpanded = parsed.organization ?? false; organizationExpanded = parsed.organization ?? false;
labSetupExpanded = parsed.labSetup ?? false;
usersContactsExpanded = parsed.usersContacts ?? false;
referenceDataExpanded = parsed.referenceData ?? false;
} catch (e) { } catch (e) {
// Keep defaults if parsing fails // Keep defaults if parsing fails
} }
@ -59,8 +63,10 @@ import {
if (browser) { if (browser) {
localStorage.setItem('sidebar_section_states', JSON.stringify({ localStorage.setItem('sidebar_section_states', JSON.stringify({
laboratory: laboratoryExpanded, laboratory: laboratoryExpanded,
masterData: masterDataExpanded, organization: organizationExpanded,
organization: organizationExpanded labSetup: labSetupExpanded,
usersContacts: usersContactsExpanded,
referenceData: referenceDataExpanded
})); }));
} }
}); });
@ -69,8 +75,10 @@ import {
$effect(() => { $effect(() => {
if (!isOpen) { if (!isOpen) {
laboratoryExpanded = false; laboratoryExpanded = false;
masterDataExpanded = false;
organizationExpanded = false; organizationExpanded = false;
labSetupExpanded = false;
usersContactsExpanded = false;
referenceDataExpanded = false;
} }
}); });
@ -96,19 +104,33 @@ function toggleLaboratory() {
laboratoryExpanded = !laboratoryExpanded; laboratoryExpanded = !laboratoryExpanded;
} }
function toggleMasterData() {
if (!isOpen) {
expandSidebar();
}
masterDataExpanded = !masterDataExpanded;
}
function toggleOrganization() { function toggleOrganization() {
if (!isOpen) { if (!isOpen) {
expandSidebar(); expandSidebar();
} }
organizationExpanded = !organizationExpanded; organizationExpanded = !organizationExpanded;
} }
function toggleLabSetup() {
if (!isOpen) {
expandSidebar();
}
labSetupExpanded = !labSetupExpanded;
}
function toggleUsersContacts() {
if (!isOpen) {
expandSidebar();
}
usersContactsExpanded = !usersContactsExpanded;
}
function toggleReferenceData() {
if (!isOpen) {
expandSidebar();
}
referenceDataExpanded = !referenceDataExpanded;
}
</script> </script>
<!-- Mobile Overlay Backdrop --> <!-- Mobile Overlay Backdrop -->
@ -212,52 +234,101 @@ function toggleLaboratory() {
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-4">Configuration</li> <li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-4">Configuration</li>
{/if} {/if}
<!-- Master Data --> <!-- Organization -->
<li class="nav-group" class:collapsed={!isOpen}> <li class="nav-group" class:collapsed={!isOpen}>
<button <button
onclick={toggleMasterData} onclick={toggleOrganization}
class="nav-link" class="nav-link"
class:centered={!isOpen} class:centered={!isOpen}
title={!isOpen ? 'Master Data' : ''} title={!isOpen ? 'Organization' : ''}
> >
<Database size={20} class="text-secondary flex-shrink-0" /> <Building2 size={20} class="text-secondary flex-shrink-0" />
{#if isOpen} {#if isOpen}
<span class="nav-text">Master Data</span> <span class="nav-text">Organization</span>
<ChevronDown size={16} class="chevron {masterDataExpanded ? 'expanded' : ''}" /> <ChevronDown size={16} class="chevron {organizationExpanded ? 'expanded' : ''}" />
{/if} {/if}
</button> </button>
{#if isOpen && masterDataExpanded} {#if isOpen && organizationExpanded}
<ul class="submenu"> <ul class="submenu">
<!-- Organization with nested submenu --> <li><a href="/master-data/organization/account" class="submenu-link"><User size={16} /> Account</a></li>
<li class="nav-group"> <li><a href="/master-data/organization/site" class="submenu-link"><LandPlot size={16} /> Site</a></li>
<button <li><a href="/master-data/organization/department" class="submenu-link"><Users size={16} /> Department</a></li>
onclick={toggleOrganization} <li><a href="/master-data/organization/discipline" class="submenu-link"><Building2 size={16} /> Discipline</a></li>
class="submenu-link" <li><a href="/master-data/organization/workstation" class="submenu-link"><Monitor size={16} /> Workstation</a></li>
> <li><a href="/master-data/organization/instrument" class="submenu-link"><Activity size={16} /> Instrument</a></li>
<Building2 size={16} /> Organization
<ChevronDown size={14} class="chevron ml-auto {organizationExpanded ? 'expanded' : ''}" />
</button>
{#if organizationExpanded}
<ul class="submenu nested">
<li><a href="/master-data/organization/account" class="submenu-link"><User size={14} /> Account</a></li>
<li><a href="/master-data/organization/site" class="submenu-link"><LandPlot size={14} /> Site</a></li>
<li><a href="/master-data/organization/department" class="submenu-link"><Users size={14} /> Department</a></li>
<li><a href="/master-data/organization/discipline" class="submenu-link"><Building2 size={14} /> Discipline</a></li>
<li><a href="/master-data/organization/workstation" class="submenu-link"><Monitor size={14} /> Workstation</a></li>
<li><a href="/master-data/organization/instrument" class="submenu-link"><Activity size={14} /> Instrument</a></li>
</ul> </ul>
{/if} {/if}
</li> </li>
<!-- Lab Setup -->
<li class="nav-group" class:collapsed={!isOpen}>
<button
onclick={toggleLabSetup}
class="nav-link"
class:centered={!isOpen}
title={!isOpen ? 'Lab Setup' : ''}
>
<FlaskConical size={20} class="text-secondary flex-shrink-0" />
{#if isOpen}
<span class="nav-text">Lab Setup</span>
<ChevronDown size={16} class="chevron {labSetupExpanded ? 'expanded' : ''}" />
{/if}
</button>
{#if isOpen && labSetupExpanded}
<ul class="submenu">
<li><a href="/master-data/containers" class="submenu-link"><FlaskConical size={16} /> Containers</a></li> <li><a href="/master-data/containers" class="submenu-link"><FlaskConical size={16} /> Containers</a></li>
<li><a href="/master-data/tests" class="submenu-link"><TestTube size={16} /> Test Definitions</a></li> <li><a href="/master-data/tests" class="submenu-link"><TestTube size={16} /> Test Definitions</a></li>
<li><a href="/master-data/testmap" class="submenu-link"><Link size={16} /> Test Mapping</a></li> <li><a href="/master-data/testmap" class="submenu-link"><Link size={16} /> Test Mapping</a></li>
</ul>
{/if}
</li>
<!-- Users & Contacts -->
<li class="nav-group" class:collapsed={!isOpen}>
<button
onclick={toggleUsersContacts}
class="nav-link"
class:centered={!isOpen}
title={!isOpen ? 'Users & Contacts' : ''}
>
<Users size={20} class="text-secondary flex-shrink-0" />
{#if isOpen}
<span class="nav-text">Users & Contacts</span>
<ChevronDown size={16} class="chevron {usersContactsExpanded ? 'expanded' : ''}" />
{/if}
</button>
{#if isOpen && usersContactsExpanded}
<ul class="submenu">
<li><a href="/master-data/users" class="submenu-link"><UserCircle size={16} /> Users</a></li> <li><a href="/master-data/users" class="submenu-link"><UserCircle size={16} /> Users</a></li>
<li><a href="/master-data/valuesets" class="submenu-link"><List size={16} /> ValueSets</a></li>
<li><a href="/master-data/locations" class="submenu-link"><MapPin size={16} /> Locations</a></li>
<li><a href="/master-data/contacts" class="submenu-link"><Users size={16} /> Contacts</a></li> <li><a href="/master-data/contacts" class="submenu-link"><Users size={16} /> Contacts</a></li>
<li><a href="/master-data/specialties" class="submenu-link"><Stethoscope size={16} /> Specialties</a></li> <li><a href="/master-data/specialties" class="submenu-link"><Stethoscope size={16} /> Specialties</a></li>
<li><a href="/master-data/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</a></li> <li><a href="/master-data/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</a></li>
</ul>
{/if}
</li>
<!-- Reference Data -->
<li class="nav-group" class:collapsed={!isOpen}>
<button
onclick={toggleReferenceData}
class="nav-link"
class:centered={!isOpen}
title={!isOpen ? 'Reference Data' : ''}
>
<Database size={20} class="text-secondary flex-shrink-0" />
{#if isOpen}
<span class="nav-text">Reference Data</span>
<ChevronDown size={16} class="chevron {referenceDataExpanded ? 'expanded' : ''}" />
{/if}
</button>
{#if isOpen && referenceDataExpanded}
<ul class="submenu">
<li><a href="/master-data/valuesets" class="submenu-link"><List size={16} /> ValueSets</a></li>
<li><a href="/master-data/locations" class="submenu-link"><MapPin size={16} /> Locations</a></li>
<li><a href="/master-data/counters" class="submenu-link"><Hash size={16} /> Counters</a></li> <li><a href="/master-data/counters" class="submenu-link"><Hash size={16} /> Counters</a></li>
<li><a href="/master-data/geography" class="submenu-link"><Globe size={16} /> Geography</a></li> <li><a href="/master-data/geography" class="submenu-link"><Globe size={16} /> Geography</a></li>
</ul> </ul>

View File

@ -323,6 +323,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Scrollable Middle Section: Editable Table --> <!-- Scrollable Middle Section: Editable Table -->
<div class="flex-1 overflow-y-auto space-y-1 py-2"> <div class="flex-1 overflow-y-auto space-y-1 py-2">

View File

@ -14,7 +14,9 @@
patientId: '', patientId: '',
patientName: '', patientName: '',
visitNumber: '', visitNumber: '',
orderNumber: '' orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
}); });
// List state // List state
@ -73,6 +75,12 @@
if (searchFilters.orderNumber.trim()) { if (searchFilters.orderNumber.trim()) {
params.OrderNumber = searchFilters.orderNumber.trim(); params.OrderNumber = searchFilters.orderNumber.trim();
} }
if (searchFilters.orderDateFrom) {
params.OrderDateFrom = searchFilters.orderDateFrom;
}
if (searchFilters.orderDateTo) {
params.OrderDateTo = searchFilters.orderDateTo;
}
const response = await fetchPatients(params); const response = await fetchPatients(params);
patients = Array.isArray(response.data) ? response.data : []; patients = Array.isArray(response.data) ? response.data : [];
@ -94,7 +102,9 @@
patientId: '', patientId: '',
patientName: '', patientName: '',
visitNumber: '', visitNumber: '',
orderNumber: '' orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
}; };
patients = []; patients = [];
selectedPatient = null; selectedPatient = null;
@ -246,7 +256,7 @@
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold text-gray-800">Laboratory Orders</h1> <h1 class="text-2xl font-bold text-gray-800">Patient Search</h1>
<p class="text-sm text-gray-600">Search patients and manage lab orders</p> <p class="text-sm text-gray-600">Search patients and manage lab orders</p>
</div> </div>
<button class="btn btn-primary btn-sm" onclick={openCreateModal}> <button class="btn btn-primary btn-sm" onclick={openCreateModal}>
@ -261,6 +271,8 @@
bind:patientName={searchFilters.patientName} bind:patientName={searchFilters.patientName}
bind:visitNumber={searchFilters.visitNumber} bind:visitNumber={searchFilters.visitNumber}
bind:orderNumber={searchFilters.orderNumber} bind:orderNumber={searchFilters.orderNumber}
bind:orderDateFrom={searchFilters.orderDateFrom}
bind:orderDateTo={searchFilters.orderDateTo}
{loading} {loading}
onSearch={handleSearch} onSearch={handleSearch}
onClear={handleClear} onClear={handleClear}

View File

@ -1,12 +1,13 @@
<script> <script>
import { Search, X } from 'lucide-svelte'; import { Search, X, User, FileUser, ClipboardList, FileText, Calendar } from 'lucide-svelte';
import { debounce } from '$lib/utils/patients.js';
/** @type {{ /** @type {{
* patientId: string, * patientId: string,
* patientName: string, * patientName: string,
* visitNumber: string, * visitNumber: string,
* orderNumber: string, * orderNumber: string,
* orderDateFrom: string,
* orderDateTo: string,
* loading: boolean, * loading: boolean,
* onSearch: () => void, * onSearch: () => void,
* onClear: () => void * onClear: () => void
@ -16,6 +17,8 @@
patientName = '', patientName = '',
visitNumber = '', visitNumber = '',
orderNumber = '', orderNumber = '',
orderDateFrom = '',
orderDateTo = '',
loading = false, loading = false,
onSearch, onSearch,
onClear onClear
@ -28,85 +31,139 @@
} }
function hasFilters() { function hasFilters() {
return patientId || patientName || visitNumber || orderNumber; return patientId || patientName || visitNumber || orderNumber || orderDateFrom || orderDateTo;
} }
</script> </script>
<div class="bg-base-100 rounded-lg shadow-sm border border-base-200 p-3"> <div class="bg-base-100 rounded-xl shadow-lg border border-base-300/50 p-3">
<div class="flex flex-wrap items-end gap-2"> <div class="flex flex-col gap-3">
<!-- Row 1: Patient ID and Patient Name -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<!-- Patient ID --> <!-- Patient ID -->
<div class="flex-1 min-w-[140px]"> <div class="form-control">
<label class="label py-0 mb-1" for="searchPatientId"> <label class="label py-0 mb-1" for="searchPatientId">
<span class="label-text text-xs text-gray-500">Patient ID</span> <span class="label-text text-xs font-medium text-gray-600">Patient ID</span>
</label> </label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<User class="w-4 h-4 text-gray-400" />
<input <input
id="searchPatientId" id="searchPatientId"
type="text" type="text"
class="input input-sm input-bordered w-full" class="grow bg-transparent outline-none"
placeholder="e.g. P001234" placeholder="e.g. P001234"
bind:value={patientId} bind:value={patientId}
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
</label>
</div> </div>
<!-- Patient Name --> <!-- Patient Name -->
<div class="flex-[2] min-w-[180px]"> <div class="form-control md:col-span-2">
<label class="label py-0 mb-1" for="searchPatientName"> <label class="label py-0 mb-1" for="searchPatientName">
<span class="label-text text-xs text-gray-500">Patient Name</span> <span class="label-text text-xs font-medium text-gray-600">Patient Name</span>
</label> </label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<FileUser class="w-4 h-4 text-gray-400" />
<input <input
id="searchPatientName" id="searchPatientName"
type="text" type="text"
class="input input-sm input-bordered w-full" class="grow bg-transparent outline-none"
placeholder="Search by name..." placeholder="Search by name..."
bind:value={patientName} bind:value={patientName}
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
</label>
</div>
</div> </div>
<!-- Row 2: Visit #, Order #, Date From, Date To -->
<div class="grid grid-cols-2 md:grid-cols-6 gap-3">
<!-- Visit Number --> <!-- Visit Number -->
<div class="flex-1 min-w-[120px]"> <div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchVisitNumber"> <label class="label py-0 mb-1" for="searchVisitNumber">
<span class="label-text text-xs text-gray-500">Visit #</span> <span class="label-text text-xs font-medium text-gray-600">Visit #</span>
</label> </label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<ClipboardList class="w-4 h-4 text-gray-400" />
<input <input
id="searchVisitNumber" id="searchVisitNumber"
type="text" type="text"
class="input input-sm input-bordered w-full" class="grow bg-transparent outline-none"
placeholder="e.g. V12345" placeholder="V12345"
bind:value={visitNumber} bind:value={visitNumber}
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
</label>
</div> </div>
<!-- Order Number --> <!-- Order Number -->
<div class="flex-1 min-w-[120px]"> <div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchOrderNumber"> <label class="label py-0 mb-1" for="searchOrderNumber">
<span class="label-text text-xs text-gray-500">Order #</span> <span class="label-text text-xs font-medium text-gray-600">Order #</span>
</label> </label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<FileText class="w-4 h-4 text-gray-400" />
<input <input
id="searchOrderNumber" id="searchOrderNumber"
type="text" type="text"
class="input input-sm input-bordered w-full" class="grow bg-transparent outline-none"
placeholder="e.g. O67890" placeholder="O67890"
bind:value={orderNumber} bind:value={orderNumber}
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
</label>
</div> </div>
<!-- Actions --> <!-- Date From -->
<div class="flex gap-1"> <div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchOrderDateFrom">
<span class="label-text text-xs font-medium text-gray-600">Order From</span>
</label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<Calendar class="w-4 h-4 text-gray-400" />
<input
id="searchOrderDateFrom"
type="date"
class="grow bg-transparent outline-none text-xs"
bind:value={orderDateFrom}
onkeydown={handleKeydown}
/>
</label>
</div>
<!-- Date To -->
<div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchOrderDateTo">
<span class="label-text text-xs font-medium text-gray-600">Order To</span>
</label>
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<Calendar class="w-4 h-4 text-gray-400" />
<input
id="searchOrderDateTo"
type="date"
class="grow bg-transparent outline-none text-xs"
bind:value={orderDateTo}
onkeydown={handleKeydown}
/>
</label>
</div>
<!-- Action Buttons -->
<div class="form-control col-span-2 flex flex-row items-end gap-2">
{#if hasFilters()} {#if hasFilters()}
<button <button
class="btn btn-sm btn-ghost" class="btn btn-ghost btn-sm flex-1"
title="Clear filters" title="Clear filters"
onclick={onClear} onclick={onClear}
> >
<X class="w-4 h-4" /> <X class="w-4 h-4" />
<span class="hidden sm:inline ml-1">Clear</span>
</button> </button>
{:else}
<div class="flex-1"></div>
{/if} {/if}
<button <button
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm flex-[2]"
onclick={onSearch} onclick={onSearch}
disabled={loading} disabled={loading}
> >
@ -114,9 +171,10 @@
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
{:else} {:else}
<Search class="w-4 h-4 mr-1" /> <Search class="w-4 h-4 mr-1" />
{/if}
Search Search
{/if}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>