Add focus-within input patterns, update Sidebar, TestMapModal, and PatientSearchBar components
This commit is contained in:
parent
ad1618efec
commit
ecc4822a38
33
AGENTS.md
33
AGENTS.md
@ -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
|
||||
|
||||
```
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user