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
- **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
```

View File

@ -34,8 +34,10 @@ import {
// Collapsible section states - default collapsed
let laboratoryExpanded = $state(false);
let masterDataExpanded = $state(false);
let organizationExpanded = $state(false);
let labSetupExpanded = $state(false);
let usersContactsExpanded = $state(false);
let referenceDataExpanded = $state(false);
// Load states from localStorage on mount
$effect(() => {
@ -45,8 +47,10 @@ import {
try {
const parsed = JSON.parse(savedStates);
laboratoryExpanded = parsed.laboratory ?? false;
masterDataExpanded = parsed.masterData ?? false;
organizationExpanded = parsed.organization ?? false;
labSetupExpanded = parsed.labSetup ?? false;
usersContactsExpanded = parsed.usersContacts ?? false;
referenceDataExpanded = parsed.referenceData ?? false;
} catch (e) {
// Keep defaults if parsing fails
}
@ -59,8 +63,10 @@ import {
if (browser) {
localStorage.setItem('sidebar_section_states', JSON.stringify({
laboratory: laboratoryExpanded,
masterData: masterDataExpanded,
organization: organizationExpanded
organization: organizationExpanded,
labSetup: labSetupExpanded,
usersContacts: usersContactsExpanded,
referenceData: referenceDataExpanded
}));
}
});
@ -69,8 +75,10 @@ import {
$effect(() => {
if (!isOpen) {
laboratoryExpanded = false;
masterDataExpanded = false;
organizationExpanded = false;
labSetupExpanded = false;
usersContactsExpanded = false;
referenceDataExpanded = false;
}
});
@ -96,19 +104,33 @@ function toggleLaboratory() {
laboratoryExpanded = !laboratoryExpanded;
}
function toggleMasterData() {
if (!isOpen) {
expandSidebar();
}
masterDataExpanded = !masterDataExpanded;
}
function toggleOrganization() {
if (!isOpen) {
expandSidebar();
}
organizationExpanded = !organizationExpanded;
}
function toggleLabSetup() {
if (!isOpen) {
expandSidebar();
}
labSetupExpanded = !labSetupExpanded;
}
function toggleUsersContacts() {
if (!isOpen) {
expandSidebar();
}
usersContactsExpanded = !usersContactsExpanded;
}
function toggleReferenceData() {
if (!isOpen) {
expandSidebar();
}
referenceDataExpanded = !referenceDataExpanded;
}
</script>
<!-- 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>
{/if}
<!-- Master Data -->
<!-- Organization -->
<li class="nav-group" class:collapsed={!isOpen}>
<button
onclick={toggleMasterData}
onclick={toggleOrganization}
class="nav-link"
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}
<span class="nav-text">Master Data</span>
<ChevronDown size={16} class="chevron {masterDataExpanded ? 'expanded' : ''}" />
<span class="nav-text">Organization</span>
<ChevronDown size={16} class="chevron {organizationExpanded ? 'expanded' : ''}" />
{/if}
</button>
{#if isOpen && masterDataExpanded}
{#if isOpen && organizationExpanded}
<ul class="submenu">
<!-- Organization with nested submenu -->
<li class="nav-group">
<button
onclick={toggleOrganization}
class="submenu-link"
>
<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>
<li><a href="/master-data/organization/account" class="submenu-link"><User size={16} /> Account</a></li>
<li><a href="/master-data/organization/site" class="submenu-link"><LandPlot size={16} /> Site</a></li>
<li><a href="/master-data/organization/department" class="submenu-link"><Users size={16} /> Department</a></li>
<li><a href="/master-data/organization/discipline" class="submenu-link"><Building2 size={16} /> Discipline</a></li>
<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>
</ul>
{/if}
</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/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>
</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/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/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>
</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/geography" class="submenu-link"><Globe size={16} /> Geography</a></li>
</ul>

View File

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

View File

@ -14,7 +14,9 @@
patientId: '',
patientName: '',
visitNumber: '',
orderNumber: ''
orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
});
// List state
@ -73,6 +75,12 @@
if (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);
patients = Array.isArray(response.data) ? response.data : [];
@ -94,7 +102,9 @@
patientId: '',
patientName: '',
visitNumber: '',
orderNumber: ''
orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
};
patients = [];
selectedPatient = null;
@ -246,7 +256,7 @@
<!-- Header -->
<div class="flex items-center justify-between">
<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>
</div>
<button class="btn btn-primary btn-sm" onclick={openCreateModal}>
@ -261,6 +271,8 @@
bind:patientName={searchFilters.patientName}
bind:visitNumber={searchFilters.visitNumber}
bind:orderNumber={searchFilters.orderNumber}
bind:orderDateFrom={searchFilters.orderDateFrom}
bind:orderDateTo={searchFilters.orderDateTo}
{loading}
onSearch={handleSearch}
onClear={handleClear}

View File

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