refactor: improve patient form validation and order management

This commit is contained in:
mahdahar 2026-03-05 16:05:42 +07:00
parent afd8028a21
commit 94af37dae5
9 changed files with 216 additions and 319 deletions

View File

@ -45,7 +45,13 @@ export async function apiClient(endpoint, options = {}) {
// Handle other errors // Handle other errors
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'An error occurred' })); const error = await response.json().catch(() => ({ message: 'An error occurred' }));
throw new Error(error.message || `HTTP error! status: ${response.status}`); // Create error object with full response data for field-specific errors
const err = new Error(error.message || `HTTP error! status: ${response.status}`);
err.status = response.status;
if (error.messages) {
err.messages = error.messages;
}
throw err;
} }
// Parse JSON response // Parse JSON response

View File

@ -23,11 +23,8 @@ function createValueSetsStore() {
* @returns {Promise<Array>} - Array of value set items * @returns {Promise<Array>} - Array of value set items
*/ */
async load(key) { async load(key) {
console.log('valuesets.load() called for key:', key);
// If there's already an in-flight request, return its promise // If there's already an in-flight request, return its promise
if (inflightRequests.has(key)) { if (inflightRequests.has(key)) {
console.log('valuesets: returning existing in-flight request for:', key);
return inflightRequests.get(key); return inflightRequests.get(key);
} }
@ -36,14 +33,11 @@ function createValueSetsStore() {
subscribe((state) => { cacheState = state; })(); subscribe((state) => { cacheState = state; })();
if (cacheState?.[key]?.loaded) { if (cacheState?.[key]?.loaded) {
console.log('valuesets: returning cached items for:', key);
return cacheState[key].items; return cacheState[key].items;
} }
// Create the fetch promise // Create the fetch promise
const fetchPromise = (async () => { const fetchPromise = (async () => {
console.log('valuesets: starting fetch for:', key);
// Mark as loading // Mark as loading
update((cache) => ({ update((cache) => ({
...cache, ...cache,
@ -51,14 +45,11 @@ function createValueSetsStore() {
})); }));
try { try {
console.log('valuesets: calling API for key:', key);
const response = await fetchValueSetByKey(key); const response = await fetchValueSetByKey(key);
console.log('valuesets: API response for', key, ':', response);
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
// Handle both response.data being an array or having an Items property // Handle both response.data being an array or having an Items property
let items = Array.isArray(response.data) ? response.data : (response.data.Items || []); let items = Array.isArray(response.data) ? response.data : (response.data.Items || []);
console.log('valuesets: items for', key, ':', items);
// Sort by Sequence if available // Sort by Sequence if available
items.sort((a, b) => (a.Sequence || 0) - (b.Sequence || 0)); items.sort((a, b) => (a.Sequence || 0) - (b.Sequence || 0));
@ -68,7 +59,6 @@ function createValueSetsStore() {
[key]: { items, loaded: true, loading: false, error: null }, [key]: { items, loaded: true, loading: false, error: null },
})); }));
console.log('valuesets: returning items for', key);
return items; return items;
} else { } else {
throw new Error(response.message || 'Failed to load value set'); throw new Error(response.message || 'Failed to load value set');

View File

@ -4,12 +4,14 @@
import { ORDER_STATUS, ORDER_PRIORITY } from '$lib/api/orders.js'; import { ORDER_STATUS, ORDER_PRIORITY } from '$lib/api/orders.js';
import { fetchPatients } from '$lib/api/patients.js'; import { fetchPatients } from '$lib/api/patients.js';
import { fetchTests } from '$lib/api/tests.js'; import { fetchTests } from '$lib/api/tests.js';
import { fetchVisitsByPatient } from '$lib/api/visits.js';
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js'; import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
import { User, FlaskConical, Building2, Hash, FileText, AlertCircle, Plus, X, Search, Beaker } from 'lucide-svelte'; import { User, FlaskConical, Building2, Hash, FileText, AlertCircle, Plus, X, Search, Beaker } from 'lucide-svelte';
/** @type {{ /** @type {{
* open: boolean, * open: boolean,
* order: Object | null, * order: Object | null,
* patient: Object | null,
* loading: boolean, * loading: boolean,
* onSave: () => void, * onSave: () => void,
* onCancel: () => void * onCancel: () => void
@ -17,6 +19,7 @@
let { let {
open = $bindable(false), open = $bindable(false),
order = null, order = null,
patient = null,
loading = false, loading = false,
onSave, onSave,
onCancel onCancel
@ -41,13 +44,18 @@
let patientSearchResults = $state([]); let patientSearchResults = $state([]);
let showPatientSearch = $state(false); let showPatientSearch = $state(false);
let selectedPatient = $state(null); let selectedPatient = $state(null);
let patientVisits = $state([]);
let visitsLoading = $state(false);
let testSearchQuery = $state(''); let testSearchQuery = $state('');
let testSearchResults = $state([]); let testSearchResults = $state([]);
let showTestSearch = $state(false); let showTestSearch = $state(false);
let isInitialized = $state(false);
// Reset form when modal opens // Reset form when modal opens (only when transitioning from closed to open)
$effect(() => { $effect(() => {
if (open) { if (open && !isInitialized) {
isInitialized = true;
if (order) { if (order) {
// Edit mode - populate form // Edit mode - populate form
formData = { formData = {
@ -79,7 +87,16 @@
Comment: '', Comment: '',
Tests: [] Tests: []
}; };
// If patient prop is provided, auto-select it
if (patient) {
selectedPatient = patient;
formData.InternalPID = patient.InternalPID;
loadPatientVisits(patient.InternalPID);
} else {
selectedPatient = null; selectedPatient = null;
}
patientSearchQuery = ''; patientSearchQuery = '';
patientSearchResults = []; patientSearchResults = [];
} }
@ -88,6 +105,9 @@
testSearchQuery = ''; testSearchQuery = '';
testSearchResults = []; testSearchResults = [];
showTestSearch = false; showTestSearch = false;
} else if (!open) {
// Reset initialization flag when modal closes
isInitialized = false;
} }
}); });
@ -113,9 +133,29 @@
function selectPatient(patient) { function selectPatient(patient) {
selectedPatient = patient; selectedPatient = patient;
formData.InternalPID = patient.InternalPID; formData.InternalPID = patient.InternalPID;
formData.PatVisitID = ''; // Reset visit when patient changes
showPatientSearch = false; showPatientSearch = false;
patientSearchQuery = ''; patientSearchQuery = '';
patientSearchResults = []; patientSearchResults = [];
loadPatientVisits(patient.InternalPID);
}
async function loadPatientVisits(internalPID) {
if (!internalPID) {
patientVisits = [];
return;
}
visitsLoading = true;
try {
const response = await fetchVisitsByPatient(internalPID);
patientVisits = response.data || [];
} catch (err) {
console.error('Failed to load patient visits:', err);
patientVisits = [];
} finally {
visitsLoading = false;
}
} }
async function searchTests() { async function searchTests() {
@ -245,15 +285,22 @@
<p class="text-xs text-gray-500">ID: {selectedPatient.PatientID}</p> <p class="text-xs text-gray-500">ID: {selectedPatient.PatientID}</p>
<p class="text-xs text-gray-500">Internal PID: {selectedPatient.InternalPID}</p> <p class="text-xs text-gray-500">Internal PID: {selectedPatient.InternalPID}</p>
</div> </div>
{#if !order} {#if !order && !patient}
<button <button
class="btn btn-ghost btn-xs btn-square" class="btn btn-ghost btn-xs btn-square"
onclick={() => { selectedPatient = null; formData.InternalPID = ''; }} onclick={() => { selectedPatient = null; formData.InternalPID = ''; formData.PatVisitID = ''; patientVisits = []; }}
> >
<X class="w-4 h-4" /> <X class="w-4 h-4" />
</button> </button>
{/if} {/if}
</div> </div>
{#if patientVisits.length > 0}
<div class="mt-2 pt-2 border-t border-base-200">
<p class="text-xs text-gray-500">
{patientVisits.length} visit{patientVisits.length === 1 ? '' : 's'} available
</p>
</div>
{/if}
</div> </div>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-2">
@ -342,18 +389,27 @@
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div class="form-control"> <div class="form-control">
<label class="label py-0.5" for="orderVisitId"> <label class="label py-0.5" for="orderVisitId">
<span class="label-text text-xs text-gray-500">Visit ID</span> <span class="label-text text-xs text-gray-500">Visit</span>
</label> </label>
<div class="input input-sm input-bordered flex items-center gap-2 focus-within:input-primary"> <select
<Hash class="w-3.5 h-3.5 text-gray-400" />
<input
id="orderVisitId" id="orderVisitId"
type="number" class="select select-sm select-bordered w-full focus:select-primary"
class="grow bg-transparent outline-none"
bind:value={formData.PatVisitID} bind:value={formData.PatVisitID}
placeholder="Optional" disabled={!selectedPatient || visitsLoading || patientVisits.length === 0}
/> >
</div> <option value="">Select visit...</option>
{#if visitsLoading}
<option value="" disabled>Loading visits...</option>
{:else if patientVisits.length === 0}
<option value="" disabled>No visits found</option>
{:else}
{#each patientVisits as visit (visit.InternalPVID)}
<option value={visit.InternalPVID}>
{visit.PVID} - {visit.PVACreateDate ? new Date(visit.PVACreateDate).toLocaleDateString() : 'N/A'}{visit.ADTCode ? ` (${visit.ADTCode})` : ''}
</option>
{/each}
{/if}
</select>
</div> </div>
<div class="form-control"> <div class="form-control">

View File

@ -1,22 +1,20 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fetchPatients, fetchPatient } from '$lib/api/patients.js'; import { fetchPatients, fetchPatient } from '$lib/api/patients.js';
import { fetchOrders, createOrder } from '$lib/api/orders.js';
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js'; import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
import PatientSearchBar from './PatientSearchBar.svelte'; import PatientSearchBar from './PatientSearchBar.svelte';
import PatientList from './PatientList.svelte'; import PatientList from './PatientList.svelte';
import OrderList from './OrderList.svelte'; import OrderList from './OrderList.svelte';
import PatientFormModal from './PatientFormModal.svelte'; import PatientFormModal from './PatientFormModal.svelte';
import OrderFormModal from '../orders/OrderFormModal.svelte';
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import { Plus, Edit2, Trash2 } from 'lucide-svelte'; import { Plus, Edit2, Trash2 } from 'lucide-svelte';
// Search state // Search state (only PatientID and Name are supported by API)
let searchFilters = $state({ let searchFilters = $state({
patientId: '', patientId: '',
patientName: '', patientName: ''
visitNumber: '',
orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
}); });
// List state // List state
@ -44,6 +42,14 @@
patient: null patient: null
}); });
// Order form state
let orderForm = $state({
open: false,
order: null,
patient: null,
loading: false
});
// Load patients on mount (empty on init) // Load patients on mount (empty on init)
onMount(() => { onMount(() => {
// Don't auto-load - wait for search // Don't auto-load - wait for search
@ -62,27 +68,16 @@
perPage perPage
}; };
// Add filters // Add filters (API expects these exact parameter names)
if (searchFilters.patientId.trim()) { if (searchFilters.patientId.trim()) {
params.PatientID = searchFilters.patientId.trim(); params.PatientID = searchFilters.patientId.trim();
} }
if (searchFilters.patientName.trim()) { if (searchFilters.patientName.trim()) {
params.Name = searchFilters.patientName.trim(); params.Name = searchFilters.patientName.trim();
} }
if (searchFilters.visitNumber.trim()) {
params.VisitNumber = searchFilters.visitNumber.trim();
}
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); const response = await fetchPatients(params);
patients = Array.isArray(response.data) ? response.data : []; patients = Array.isArray(response.data) ? response.data : [];
if (response.pagination) { if (response.pagination) {
@ -100,11 +95,7 @@
function handleClear() { function handleClear() {
searchFilters = { searchFilters = {
patientId: '', patientId: '',
patientName: '', patientName: ''
visitNumber: '',
orderNumber: '',
orderDateFrom: '',
orderDateTo: ''
}; };
patients = []; patients = [];
selectedPatient = null; selectedPatient = null;
@ -130,40 +121,8 @@
ordersLoading = true; ordersLoading = true;
try { try {
// TODO: Replace with actual orders API endpoint const response = await fetchOrders({ InternalPID: patient.InternalPID });
// const response = await fetchOrdersByPatient(patient.InternalPID); orders = Array.isArray(response.data) ? response.data : [];
// Mock orders for now - remove when API is ready
orders = [
{
OrderID: 'ORD001',
OrderNumber: 'O2024001',
PatientID: patient.PatientID,
TestCode: 'CBC',
TestName: 'Complete Blood Count',
Status: 'pending',
OrderDate: new Date().toISOString(),
Priority: 'normal',
OrderedBy: 'Dr. Smith',
ResultCount: 0,
HasResults: false
},
{
OrderID: 'ORD002',
OrderNumber: 'O2024002',
PatientID: patient.PatientID,
TestCode: 'GLU',
TestName: 'Glucose',
Status: 'completed',
OrderDate: new Date(Date.now() - 86400000).toISOString(),
Priority: 'urgent',
OrderedBy: 'Dr. Jones',
CollectionDate: new Date(Date.now() - 80000000).toISOString(),
VerifiedDate: new Date(Date.now() - 40000000).toISOString(),
ResultCount: 3,
HasResults: true
}
];
} catch (err) { } catch (err) {
toastError(err.message || 'Failed to load orders'); toastError(err.message || 'Failed to load orders');
orders = []; orders = [];
@ -177,12 +136,6 @@
loadOrders(patient); loadOrders(patient);
} }
function handleSelectPatient(patient) {
// Just select the patient, don't auto-load orders
selectedPatient = patient;
orders = [];
}
// Patient CRUD // Patient CRUD
function openCreateModal() { function openCreateModal() {
patientForm = { open: true, patient: null, loading: false }; patientForm = { open: true, patient: null, loading: false };
@ -231,8 +184,32 @@
// Order actions // Order actions
function openCreateOrder() { function openCreateOrder() {
// TODO: Implement order creation modal if (!selectedPatient) {
toastSuccess('Create order - implement modal'); toastError('Please select a patient first');
return;
}
orderForm = { open: true, order: null, patient: selectedPatient, loading: false };
}
async function handleOrderSaved(formData) {
orderForm.loading = true;
try {
await createOrder(formData);
toastSuccess('Order created successfully');
orderForm.open = false;
// Refresh orders list
if (selectedPatient) {
await loadOrders(selectedPatient);
}
} catch (err) {
toastError(err.message || 'Failed to create order');
} finally {
orderForm.loading = false;
}
}
function handleOrderCancel() {
orderForm.open = false;
} }
function handleViewOrder(order) { function handleViewOrder(order) {
@ -269,10 +246,6 @@
<PatientSearchBar <PatientSearchBar
bind:patientId={searchFilters.patientId} bind:patientId={searchFilters.patientId}
bind:patientName={searchFilters.patientName} bind:patientName={searchFilters.patientName}
bind:visitNumber={searchFilters.visitNumber}
bind:orderNumber={searchFilters.orderNumber}
bind:orderDateFrom={searchFilters.orderDateFrom}
bind:orderDateTo={searchFilters.orderDateTo}
{loading} {loading}
onSearch={handleSearch} onSearch={handleSearch}
onClear={handleClear} onClear={handleClear}
@ -297,8 +270,7 @@
{totalItems} {totalItems}
{perPage} {perPage}
onPageChange={handlePageChange} onPageChange={handlePageChange}
onSelectPatient={handleSelectPatient} onSelectPatient={handleShowOrders}
onShowOrders={handleShowOrders}
/> />
</div> </div>
@ -331,6 +303,16 @@
loading={patientForm.loading} loading={patientForm.loading}
/> />
<!-- Order Form Modal -->
<OrderFormModal
bind:open={orderForm.open}
order={orderForm.order}
patient={orderForm.patient}
onSave={handleOrderSaved}
onCancel={handleOrderCancel}
loading={orderForm.loading}
/>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<Modal bind:open={deleteModal.open} title="Confirm Delete" size="sm"> <Modal bind:open={deleteModal.open} title="Confirm Delete" size="sm">
<div class="py-2"> <div class="py-2">

View File

@ -1,6 +1,6 @@
<script> <script>
import { FileText, Printer, Eye, Clock, CheckCircle, AlertCircle } from 'lucide-svelte'; import { Printer, AlertCircle } from 'lucide-svelte';
import { formatDateTime, formatDate, getOrderStatusClass } from '$lib/utils/patients.js'; import { formatDate } from '$lib/utils/patients.js';
/** /**
* @typedef {Object} Order * @typedef {Object} Order
@ -23,84 +23,27 @@
/** @type {{ order: Order, onView: () => void, onPrintBarcode: () => void }} */ /** @type {{ order: Order, onView: () => void, onPrintBarcode: () => void }} */
let { order, onView, onPrintBarcode } = $props(); let { order, onView, onPrintBarcode } = $props();
let statusClass = $derived(getOrderStatusClass(order?.Status));
let isUrgent = $derived(order?.Priority?.toLowerCase() === 'urgent' || order?.Priority?.toLowerCase() === 'stat'); let isUrgent = $derived(order?.Priority?.toLowerCase() === 'urgent' || order?.Priority?.toLowerCase() === 'stat');
</script> </script>
<div class="card bg-base-100 shadow-sm border border-base-200 hover:shadow-md transition-shadow" <div class="flex items-center justify-between py-1.5 px-2 text-sm border-b border-base-200 hover:bg-base-200 cursor-pointer"
class:border-error={isUrgent}> class:border-error={isUrgent}
<div class="card-body p-3 compact-y"> onclick={onView}>
<div class="flex items-start justify-between gap-2"> <div class="flex items-center gap-3 min-w-0 flex-1">
<div class="flex-1 min-w-0">
<!-- Order Header -->
<div class="flex items-center gap-2 mb-1">
{#if isUrgent} {#if isUrgent}
<AlertCircle class="w-4 h-4 text-error" /> <AlertCircle class="w-3 h-3 text-error flex-shrink-0" />
{/if} {/if}
<span class="font-semibold text-sm truncate"> <span class="font-medium truncate w-24">{order.OrderNumber || order.OrderID || '-'}</span>
{order.TestName || order.TestCode || 'Unknown Test'} <span class="text-gray-600 text-xs w-24">{formatDate(order.OrderDate)}</span>
</span> <span class="text-gray-600 text-xs truncate flex-1">{order.OrderedBy || '-'}</span>
<span class="badge badge-sm {statusClass}">
{order.Status || 'Unknown'}
</span>
</div> </div>
<div class="flex gap-1">
<!-- Order Details -->
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-gray-600">
<div class="flex items-center gap-1">
<FileText class="w-3 h-3" />
<span class="truncate">{order.OrderNumber || order.OrderID || '-'}</span>
</div>
<div class="flex items-center gap-1">
<Clock class="w-3 h-3" />
<span>{formatDateTime(order.OrderDate)}</span>
</div>
{#if order.OrderedBy}
<div class="col-span-2 truncate">
<span class="text-gray-400">Ordered by:</span> {order.OrderedBy}
</div>
{/if}
{#if order.CollectionDate}
<div class="flex items-center gap-1">
<CheckCircle class="w-3 h-3 text-success" />
<span>Collected: {formatDate(order.CollectionDate)}</span>
</div>
{/if}
</div>
<!-- Results Indicator -->
{#if order.HasResults || order.ResultCount > 0}
<div class="mt-2 flex items-center gap-2">
<span class="badge badge-sm badge-primary">
{order.ResultCount || '?'} Results
</span>
{#if order.VerifiedDate}
<span class="text-xs text-success flex items-center gap-1">
<CheckCircle class="w-3 h-3" />
Verified
</span>
{/if}
</div>
{/if}
</div>
<!-- Action Buttons -->
<div class="flex flex-col gap-1">
<button <button
class="btn btn-sm btn-ghost" class="btn btn-xs btn-ghost"
title="View Order"
onclick={onView}
>
<Eye class="w-4 h-4" />
</button>
<button
class="btn btn-sm btn-ghost"
title="Print Barcode" title="Print Barcode"
onclick={onPrintBarcode} onclick={(e) => { e.stopPropagation(); onPrintBarcode(); }}
> >
<Printer class="w-4 h-4" /> <Printer class="w-3 h-3" />
</button> </button>
</div> </div>
</div> </div>
</div>
</div>

View File

@ -96,7 +96,14 @@
</button> </button>
</div> </div>
{:else} {:else}
<div class="space-y-2"> <!-- Compact Orders Table Header -->
<div class="flex items-center py-1 px-2 text-xs font-medium text-gray-500 border-b border-base-300 bg-base-100">
<span class="w-24">Order #</span>
<span class="w-24">Date</span>
<span class="flex-1">Doctor</span>
<span class="w-8"></span>
</div>
<div class="divide-y divide-base-200">
{#each orders as order (order.OrderID || order.OrderNumber)} {#each orders as order (order.OrderID || order.OrderNumber)}
<OrderCard <OrderCard
{order} {order}
@ -113,7 +120,7 @@
<div class="text-center"> <div class="text-center">
<FileText class="w-16 h-16 mx-auto mb-4 opacity-50" /> <FileText class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p class="text-lg">Select a patient</p> <p class="text-lg">Select a patient</p>
<p class="text-sm">Click "Show Orders" on a patient to view lab orders</p> <p class="text-sm">Click a patient to view lab orders</p>
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -231,7 +231,12 @@
open = false; open = false;
onSave?.(); onSave?.();
} catch (err) { } catch (err) {
toastError(err.message || 'Failed to save patient'); // Show API validation errors on modal
if (err.messages && typeof err.messages === 'object') {
formErrors = err.messages;
} else {
formErrors = { general: err.message || 'Failed to save patient' };
}
} finally { } finally {
saving = false; saving = false;
} }
@ -268,6 +273,16 @@
</div> </div>
{:else} {:else}
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}> <form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
<!-- Error Alert -->
{#if Object.keys(formErrors).length > 0}
<div class="alert alert-error alert-sm">
<div class="flex flex-col">
{#each Object.entries(formErrors) as [field, msg]}
<span class="text-sm">{field}: {msg}</span>
{/each}
</div>
</div>
{/if}
<!-- DaisyUI Tabs --> <!-- DaisyUI Tabs -->
<div class="tabs tabs-bordered"> <div class="tabs tabs-bordered">
<button <button

View File

@ -27,8 +27,7 @@
* totalItems: number, * totalItems: number,
* perPage: number, * perPage: number,
* onPageChange: (page: number) => void, * onPageChange: (page: number) => void,
* onSelectPatient: (patient: Patient) => void, * onSelectPatient: (patient: Patient) => void
* onShowOrders: (patient: Patient) => void
* }} */ * }} */
let { let {
patients = [], patients = [],
@ -39,16 +38,14 @@
totalItems = 0, totalItems = 0,
perPage = 20, perPage = 20,
onPageChange, onPageChange,
onSelectPatient, onSelectPatient
onShowOrders
} = $props(); } = $props();
const columns = [ const columns = [
{ key: 'PatientID', label: 'Patient ID', class: 'font-medium w-24' }, { key: 'PatientID', label: 'ID', class: 'font-medium w-20' },
{ key: 'FullName', label: 'Name', class: 'min-w-32' }, { key: 'FullName', label: 'Name', class: 'min-w-32' },
{ key: 'SexLabel', label: 'Sex', class: 'w-16' }, { key: 'SexLabel', label: 'Sex', class: 'w-12' },
{ key: 'BirthdateFormatted', label: 'Birthdate', class: 'w-28' }, { key: 'BirthdateFormatted', label: 'DOB', class: 'w-24' },
{ key: 'actions', label: 'Orders', class: 'w-24 text-center' },
]; ];
let displayPatients = $derived( let displayPatients = $derived(
@ -60,10 +57,6 @@
})) }))
); );
function handleShowOrders(patient, event) {
event.stopPropagation();
onShowOrders(patient);
}
</script> </script>
<div class="bg-base-100 rounded-lg shadow border border-base-200 flex flex-col h-full overflow-hidden"> <div class="bg-base-100 rounded-lg shadow border border-base-200 flex flex-col h-full overflow-hidden">
@ -88,17 +81,6 @@
onRowClick={onSelectPatient} onRowClick={onSelectPatient}
> >
{#snippet cell({ column, row })} {#snippet cell({ column, row })}
{#if column.key === 'actions'}
<div class="flex justify-center">
<button
class="btn btn-xs btn-primary"
class:btn-outline={selectedPatient?.InternalPID !== row.InternalPID}
onclick={(e) => handleShowOrders(row, e)}
>
Show
</button>
</div>
{:else}
<span <span
class="truncate block" class="truncate block"
class:font-semibold={selectedPatient?.InternalPID === row.InternalPID} class:font-semibold={selectedPatient?.InternalPID === row.InternalPID}
@ -106,7 +88,6 @@
> >
{row[column.key]} {row[column.key]}
</span> </span>
{/if}
{/snippet} {/snippet}
</DataTable> </DataTable>
</div> </div>

View File

@ -1,24 +1,16 @@
<script> <script>
import { Search, X, User, FileUser, ClipboardList, FileText, Calendar } from 'lucide-svelte'; import { Search, X, User, FileUser } from 'lucide-svelte';
/** @type {{ /** @type {{
* patientId: string, * patientId: string,
* patientName: string, * patientName: string,
* visitNumber: string,
* orderNumber: string,
* orderDateFrom: string,
* orderDateTo: string,
* loading: boolean, * loading: boolean,
* onSearch: () => void, * onSearch: () => void,
* onClear: () => void * onClear: () => void
* }} */ * }} */
let { let {
patientId = '', patientId = $bindable(''),
patientName = '', patientName = $bindable(''),
visitNumber = '',
orderNumber = '',
orderDateFrom = '',
orderDateTo = '',
loading = false, loading = false,
onSearch, onSearch,
onClear onClear
@ -31,7 +23,7 @@
} }
function hasFilters() { function hasFilters() {
return patientId || patientName || visitNumber || orderNumber || orderDateFrom || orderDateTo; return patientId || patientName;
} }
</script> </script>
@ -76,94 +68,20 @@
</div> </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 -->
<div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchVisitNumber">
<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="grow bg-transparent outline-none"
placeholder="V12345"
bind:value={visitNumber}
onkeydown={handleKeydown}
/>
</label>
</div>
<!-- Order Number -->
<div class="form-control col-span-1">
<label class="label py-0 mb-1" for="searchOrderNumber">
<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="grow bg-transparent outline-none"
placeholder="O67890"
bind:value={orderNumber}
onkeydown={handleKeydown}
/>
</label>
</div>
<!-- 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 --> <!-- Action Buttons -->
<div class="form-control col-span-2 flex flex-row items-end gap-2"> <div class="flex flex-row items-end gap-2 mt-2">
{#if hasFilters()} {#if hasFilters()}
<button <button
class="btn btn-ghost btn-sm flex-1" class="btn btn-ghost btn-sm"
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> <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 flex-[2]" class="btn btn-primary btn-sm flex-1"
onclick={onSearch} onclick={onSearch}
disabled={loading} disabled={loading}
> >
@ -177,4 +95,3 @@
</div> </div>
</div> </div>
</div> </div>
</div>