first commit

This commit is contained in:
mahdahar 2026-02-08 17:39:53 +07:00
commit bae48fab29
37 changed files with 7900 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/.serena

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

162
AGENTS.md Normal file
View File

@ -0,0 +1,162 @@
# CLQMS Frontend - Agent Guidelines
## Build Commands
```bash
# Development
pnpm dev # Start dev server (http://localhost:5173)
# Production
pnpm build # Build for production (outputs to build/)
pnpm preview # Preview production build locally
# Type Checking
pnpm check # Run svelte-check TypeScript validation
pnpm check:watch # Run type checker in watch mode
# Package Management
pnpm install # Install dependencies
pnpm prepare # Sync SvelteKit (runs automatically)
```
## Code Style Guidelines
### TypeScript & Svelte
- Use TypeScript strict mode (configured in tsconfig.json)
- Prefer explicit types over `any`
- Use Svelte 5 runes: `$state`, `$derived`, `$effect`, `$props`
- Components use `.svelte` extension with `<script lang="ts">`
### Imports & Aliases
Use path aliases (defined in svelte.config.js):
- `$components/*``src/lib/components/*`
- `$stores/*``src/lib/stores/*`
- `$types/*``src/lib/types/*`
- `$server/*``src/lib/server/*`
- `$lib/*``src/lib/*` (built-in)
Order: External libs → SvelteKit → Internal aliases → Relative imports
### Naming Conventions
- Components: PascalCase (e.g., `Sidebar.svelte`, `LoginForm.svelte`)
- Files: kebab-case for non-components (e.g., `api-client.ts`)
- Variables/functions: camelCase
- Constants: UPPER_SNAKE_CASE
- Types/Interfaces: PascalCase with descriptive names
- Svelte stores: camelCase (e.g., `auth`, `userStore`)
### Component Structure
```svelte
<script lang="ts">
// 1. Imports
import { goto } from '$app/navigation';
import { auth } from '$stores/auth';
// 2. Props
let { title, data } = $props<{ title: string; data: SomeType }>();
// 3. State
let loading = $state(false);
// 4. Derived
let isValid = $derived(data && data.length > 0);
// 5. Effects
$effect(() => { if (isValid) console.log('Data loaded'); });
</script>
<svelte:head><title>{title}</title></svelte:head>
<!-- Template -->
<div class="card"><slot /></div>
```
### Styling (Tailwind + DaisyUI)
- Use Tailwind utility classes
- DaisyUI components: `btn`, `card`, `input`, `modal`, etc.
- Theme: Forest (medical green primary: #2d6a4f)
- Color utilities: `text-emerald-600`, `bg-emerald-100`
- Responsive: `sm:`, `md:`, `lg:` prefixes
- Spacing: 4px base unit (e.g., `p-4` = 16px)
### Error Handling
```typescript
import { api } from '$server/api';
async function fetchData() {
const { data, error, success } = await api.get<Patient[]>('/api/patients');
if (!success || error) {
console.error('Failed to fetch:', error?.message);
return;
}
// Use data
}
```
### Form Handling (Superforms + Zod)
```typescript
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
const schema = z.object({
username: z.string().min(1, 'Required'),
email: z.string().email('Invalid email')
});
const { form, errors, enhance } = superForm(data?.form, {
validators: zodClient(schema)
});
```
### State Management
```typescript
import { writable } from 'svelte/store';
interface AuthState {
user: User | null;
isAuthenticated: boolean;
}
function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>({
user: null,
isAuthenticated: false
});
return {
subscribe,
login: (user: User) => update(s => ({ ...s, user, isAuthenticated: true })),
logout: () => set({ user: null, isAuthenticated: false })
};
}
export const auth = createAuthStore();
```
### Route Structure
```
src/routes/
├── +layout.svelte # Root layout
├── +page.svelte # Landing page (redirects)
├── (app)/ # Group: Protected routes
│ ├── +layout.svelte # App layout with sidebar
│ └── dashboard/
│ └── +page.svelte
└── (auth)/ # Group: Public routes
├── +layout.svelte # Auth layout (centered)
└── login/
└── +page.svelte
```
### API Client Usage
```typescript
import { api } from '$server/api';
// GET
const { data } = await api.get<User[]>('/api/users');
// POST
const { data } = await api.post<User>('/api/users', { name: 'John' });
// PATCH
const { data } = await api.patch<User>('/api/users/1', { name: 'Jane' });
// DELETE
const { data } = await api.delete('/api/users/1');
```
### Environment Variables
```typescript
// Use only PUBLIC_ prefix for client-side
import { PUBLIC_API_BASE_URL } from '$env/static/public';
```
### Git Workflow
- Commit messages: imperative mood (e.g., "Add patient form validation")
- Branch naming: `feature/`, `fix/`, `refactor/` prefixes
- Never commit secrets or `.env.local`
## Testing (To Be Configured)
Test framework not yet configured. When added:
- Unit tests: Vitest (planned per checklist)
- E2E tests: Playwright (planned per checklist)

734
CLQMS-CHECKLIST.md Normal file
View File

@ -0,0 +1,734 @@
# CLQMS Frontend Development Checklist
> **Framework:** SvelteKit
> **Key Change:** Value Sets moved to Phase 3 (from Phase 10) - needed for dropdowns in all forms
## Phase 1: Project Setup
- [x] Initialize project with SvelteKit
- [x] Install and configure TypeScript
- [ ] Set up ESLint and Prettier
- [x] Create SvelteKit folder structure (`src/routes/`, `src/lib/components/`, `src/lib/stores/`, `src/lib/server/`)
- [x] Configure SvelteKit adapter (auto/vercel/node for deployment target)
- [x] Set up API client using SvelteKit's fetch or axios wrapper
- [x] Configure handle hook for auth middleware (`src/hooks.server.ts`)
- [x] Set up Svelte stores for state management
- [x] Install UI component library or design system (Skeleton, Flowbite, or custom)
- [x] Create base layout components (`+layout.svelte` with Header, Sidebar, Footer)
- [x] Configure environment variables (`.env`, `.env.local`)
- [x] Set up build scripts (`vite build`, `vite preview`)
- [x] Configure form validation library (Superforms with Zod)
- [ ] Configure CI/CD pipeline (GitHub Actions/GitLab CI)
- [x] Create README with setup instructions
**Acceptance Criteria:**
- [x] Project builds without errors (`npm run build`)
- [ ] Linting and formatting configured
- [ ] API client successfully connects to backend
- [x] SvelteKit file-based routing works
- [x] Handle hook properly intercepts requests
---
## Phase 2: Authentication Module
- [x] Create login page component
- [x] Implement login form with username/password fields
- [x] Add form validation (required fields)
- [ ] Implement login API call to `/api/auth/login`
- [ ] Handle JWT token from HTTP-only cookie
- [ ] Create logout functionality (call `/api/auth/logout`)
- [ ] Implement auth state management (store user data, auth status)
- [ ] Create protected route using SvelteKit `+page.server.ts` load functions or `+layout.server.ts`
- [ ] Implement auth check using SvelteKit hooks (`src/hooks.server.ts`)
- [ ] Handle 401 errors globally (redirect to login)
- [ ] Create password change form (current password, new password)
- [ ] Implement password change API call to `/api/auth/change_pass`
- [ ] Add loading states for auth operations
- [ ] Add error messages for failed login attempts
- [ ] Create registration page (optional, if needed)
**Acceptance Criteria:**
- User can login successfully
- JWT token is stored/managed correctly
- Protected routes redirect unauthenticated users
- Logout clears auth state and token
- Password change works with proper validation
---
## Phase 4: Organization Module
### Accounts
- [ ] Create accounts list page
- [ ] Implement account creation form
- [ ] Implement account edit form
- [ ] Implement account delete with confirmation
- [ ] Add search/filter for accounts
### Sites
- [ ] Create sites list page (filtered by selected account)
- [ ] Implement site creation form (with AccountID)
- [ ] Implement site edit form
- [ ] Implement site delete with confirmation
- [ ] Add site search/filter
### Disciplines
- [ ] Create disciplines list page
- [ ] Implement discipline creation form
- [ ] Implement discipline edit form
- [ ] Implement discipline delete with confirmation
- [ ] Add discipline search/filter
### Departments
- [ ] Create departments list page (filtered by site)
- [ ] Implement department creation form (with SiteID)
- [ ] Implement department edit form
- [ ] Implement department delete with confirmation
- [ ] Add department search/filter
### Workstations
- [ ] Create workstations list page (filtered by department)
- [ ] Implement workstation creation form (with SiteID, DepartmentID)
- [ ] Implement workstation edit form
- [ ] Implement workstation delete with confirmation
- [ ] Add workstation search/filter
### Hierarchy Navigation
- [ ] Create organization tree view component
- [ ] Implement breadcrumb navigation
- [ ] Add filters to cascade (Account → Site → Department → Workstation)
**Acceptance Criteria:**
- All CRUD operations work for each entity
- Filtering and cascading selection works
- Delete operations have confirmation dialogs
- List pages support pagination
---
## Phase 5: Master Data Module
### Locations
- [ ] Create locations list page
- [ ] Implement location creation/edit form
- [ ] Implement location delete with confirmation
- [ ] Add search/filter by location code, name, type
### Contacts
- [ ] Create contacts list page
- [ ] Implement contact creation/edit form
- [ ] Implement contact delete with confirmation
- [ ] Add search/filter by name, type, specialty
### Occupations
- [ ] Create occupations list page
- [ ] Implement occupation creation/edit form
- [ ] Implement occupation delete with confirmation
### Medical Specialties
- [ ] Create medical specialties list page
- [ ] Implement specialty creation/edit form
- [ ] Implement specialty delete with confirmation
### Counters
- [ ] Create counters list page
- [ ] Implement counter creation/edit form
- [ ] Implement counter delete with confirmation
- [ ] Add ability to reset counter value
### Geographical Areas
- [ ] Create provinces list page (API: `/api/areageo/provinces`)
- [ ] Create cities list page (API: `/api/areageo/cities` with province_id filter)
- [ ] Create province → city dropdown components
- [ ] Add caching for geographical data
**Acceptance Criteria:**
- All master data CRUD operations work
- Province/city dropdowns cascade correctly
- Search and filtering works across all entities
- Geographical data is cached appropriately
---
## Phase 6: Patient Management Module
### Patient List & Search
- [ ] Create patients list page
- [ ] Implement search by PatientID or name (API parameter)
- [ ] Add pagination controls
- [ ] Implement patient detail view (click to view)
- [ ] Add quick actions (view, edit, delete)
### Patient Registration
- [ ] Create patient registration form with all fields
- [ ] Implement field validation:
- [ ] PatientID: alphanumeric, max 30 chars, pattern validation
- [ ] Sex: required, dropdown (1=Female, 2=Male)
- [ ] NameFirst: required, 1-60 chars, letters/spaces/periods
- [ ] Birthdate: required, date-time picker
- [ ] Email: format validation
- [ ] Phone/Mobile: regex validation (8-15 digits, optional +)
- [ ] ZIP: numeric only, max 10 chars
- [ ] Add optional fields (NameMiddle, NameMaiden, NameLast, etc.)
- [ ] Add identifier section (IdentifierType, Identifier)
- [ ] Implement patient check existence (API: `/api/patient/check`)
- [ ] Add duplicate patient warning if exists
### Patient Edit
- [ ] Create patient edit form (same as registration)
- [ ] Pre-fill with existing patient data
- [ ] Implement PATCH API call
- [ ] Add save confirmation
### Patient Delete
- [ ] Implement delete confirmation dialog
- [ ] Call DELETE API with PatientID
- [ ] Show success/error feedback
### Patient Visits
- [ ] Create visits list page
- [ ] Filter visits by PatientID
- [ ] Implement visit creation form
- [ ] Implement visit edit form
- [ ] Implement visit delete with confirmation
- [ ] Add visit detail view
### ADT Events
- [ ] Create ADT event form
- [ ] Implement ADT event types:
- [ ] A01: Admit
- [ ] A02: Transfer
- [ ] A03: Discharge
- [ ] A04: Register
- [ ] A08: Update
- [ ] Add validation for ADT events
- [ ] Call `/api/patvisitadt` endpoint
**Acceptance Criteria:**
- Patient CRUD operations work with proper validation
- Search finds patients by ID or name
- Duplicate patient check prevents duplicates
- Visit management works correctly
- ADT events are handled properly
---
## Phase 7: Test Catalog Module
### Test Definitions
- [ ] Create test definitions list page
- [ ] Add filter by TestType (TEST, PARAM, CALC, GROUP, TITLE)
- [ ] Add filter by DisciplineID
- [ ] Add filter by DepartmentID
- [ ] Add pagination
- [ ] Implement test detail view
- [ ] Display test metadata (code, name, unit, formula, specimen type)
### Test Browser
- [ ] Create test browser/search component
- [ ] Add search by TestCode or TestName
- [ ] Add filter by SpecimenType
- [ ] Add multi-select functionality (for ordering)
- [ ] Display test details on hover/click
### Panels & Groups
- [ ] Identify and display test groups (TestType = GROUP)
- [ ] Expand/collapse group functionality
- [ ] Show tests within groups
- [ ] Add section headers (TestType = TITLE)
### Test Mapping
- [ ] Create test mapping view (if needed for admin)
- [ ] Display HostCode ↔ ClientCode mappings
- [ ] Filter by HostType or ClientType
**Acceptance Criteria:**
- Test catalog browsable with filters
- Multi-select works for test ordering
- Groups and panels display correctly
- Search returns relevant results
---
## Phase 8: Specimen Module
### Specimen Management
- [ ] Create specimens list page
- [ ] Add filter by SpecimenStatus
- [ ] Add filter by PatientID
- [ ] Add filter by SpecimenType
- [ ] Implement specimen creation form
- [ ] Implement specimen edit form
- [ ] Implement specimen status update
- [ ] Add specimen detail view
### Container Definitions
- [ ] Create container definitions list page
- [ ] Implement container creation form (code, name, category, size, cap color)
- [ ] Implement container edit form
- [ ] Implement container delete with confirmation
- [ ] Display container metadata in specimen view
### Specimen Preparations
- [ ] Create preparations list page
- [ ] Implement preparation creation form
- [ ] Implement preparation edit form
- [ ] Implement preparation delete
### Specimen Statuses
- [ ] Create specimen statuses list page
- [ ] Implement status creation form
- [ ] Implement status edit form
- [ ] Display status activity labels
### Collection Methods
- [ ] Create collection methods list page
- [ ] Implement method creation form (code, name, method, additive, role)
- [ ] Implement method edit form
- [ ] Display method metadata
### Specimen Tracking
- [ ] Create specimen timeline/status view
- [ ] Visual status indicators (color-coded)
- [ ] History log display (if available)
- [ ] Barcode display (SpecimenID)
**Acceptance Criteria:**
- Specimen CRUD operations work
- Containers, preparations, statuses, methods manageable
- Specimen status tracking is visible
- Filters work correctly
---
## Phase 9: Orders Module
### Orders List
- [ ] Create orders list page
- [ ] Add filter by OrderStatus (pending, in-progress, completed, cancelled)
- [ ] Add filter by PatientID
- [ ] Add filter by date range
- [ ] Add pagination
- [ ] Display order priority (color-coded: stat=red, urgent=orange, routine=gray)
- [ ] Display order status badges
- [ ] Add quick actions (view, edit, delete)
### Order Creation
- [ ] Create order creation form
- [ ] Patient selection (search by PatientID)
- [ ] Visit selection (optional, dropdown)
- [ ] Priority selection (routine, stat, urgent)
- [ ] Site selection (dropdown)
- [ ] Requesting physician (text input)
- [ ] Test selection (multi-select from test catalog)
- [ ] Specimen type selection per test (if needed)
- [ ] Validate required fields (PatientID, Tests)
- [ ] Call POST `/api/ordertest`
### Order Detail View
- [ ] Display order information (OrderID, PatientID, VisitID, Date, Status, Priority)
- [ ] Display ordered tests with specimen types
- [ ] Display current status
- [ ] Add edit button
- [ ] Add delete button
- [ ] Add status change action
### Order Status Update
- [ ] Implement status change dropdown
- [ ] Call POST `/api/ordertest/status` with OrderID and OrderStatus
- [ ] Update local state on success
- [ ] Add confirmation for status changes
### Order Cancellation
- [ ] Add cancel button
- [ ] Show confirmation dialog
- [ ] Update status to 'cancelled'
- [ ] Show success/error feedback
### Order Deletion
- [ ] Implement delete confirmation
- [ ] Call DELETE `/api/ordertest`
- [ ] Show success/error feedback
**Acceptance Criteria:**
- Order creation works with all required fields
- Order list filters work
- Priority and status display correctly
- Status updates work
- Cancellation and deletion work with confirmation
---
## Phase 10: Results Module
### Results List
- [ ] Create results list page
- [ ] Add filter by PatientID
- [ ] Add filter by date range
- [ ] Add pagination
- [ ] Display patient info with results
- [ ] Add click to view details
### Results Detail View
- [ ] Display patient information
- [ ] Display order information
- [ ] Display test results in table format
- [ ] Show result values with units
- [ ] Display flags (H=High, L=Low, N=Normal, A=Abnormal) with color coding
- [ ] Highlight abnormal/critical results
- [ ] Add result reference ranges (if available)
### Results Entry (Manual)
- [ ] Create results entry form
- [ ] Select order (from pending orders)
- [ ] Display tests for order
- [ ] Input result values for each test
- [ ] Validate result values (numeric for quantitative tests)
- [ ] Auto-calculate flags (H/L/N/A) if ranges available
- [ ] Add units display
- [ ] Add remarks/comment field
- [ ] Save results to API
### Results Approval Workflow
- [ ] Add "Review" status for results
- [ ] Add "Approved" status
- [ ] Implement approve/reject actions (if required)
- [ ] Show reviewer info
- [ ] Lock approved results (if needed)
### Critical Results Alerting
- [ ] Identify critical results (flagged as critical)
- [ ] Show prominent alerts/warnings
- [ ] Add "acknowledge" action for critical results
- [ ] Log critical result acknowledgments
### Results Export/Print
- [ ] Add print results button
- [ ] Generate printable report format
- [ ] Add export to PDF
- [ ] Add export to CSV (if needed)
**Acceptance Criteria:**
- Results display correctly with flags
- Abnormal results highlighted
- Manual entry works with validation
- Approval workflow works (if implemented)
- Critical results are prominent
- Print/export functions work
---
## Phase 3: Value Sets Module
### Value Set Definitions
- [ ] Create value sets list page
- [ ] Display VSetCode, VSetName, Description, Category
- [ ] Add search/filter by code or name
- [ ] Add refresh button (call `/api/valueset/refresh`)
- [ ] Show last refresh time
### Value Set Items
- [ ] Create value set items page
- [ ] Filter by VSetID
- [ ] Display items in list/table (VValue, VLabel, VSeq, IsActive)
- [ ] Implement item creation form
- [ ] Implement item edit form
- [ ] Implement item delete with confirmation
- [ ] Add active/inactive toggle
- [ ] Implement sorting by VSeq
### Dynamic Dropdowns
- [ ] Create reusable dropdown component using value sets
- [ ] Load dropdown options from `/api/valueset/items?VSetID=X`
- [ ] Cache dropdown options in store
- [ ] Implement refresh/reload dropdown data
- [ ] Use value sets for:
- [ ] Patient prefixes
- [ ] Patient marital status
- [ ] Specimen types
- [ ] Collection methods
- [ ] Specimen statuses
- [ ] Order priorities
- [ ] Order statuses
- [ ] Test types
- [ ] Any other configurable dropdowns
### Value Set Caching
- [ ] Implement cache strategy for value sets
- [ ] Load value sets on app initialization
- [ ] Refresh cache when `/api/valueset/refresh` called
- [ ] Use stale-while-revalidate pattern for dropdowns
**Acceptance Criteria:**
- Value sets display correctly
- Items CRUD works
- Refresh clears cache and reloads
- Dynamic dropdowns populate from value sets
- Caching reduces API calls
---
## Phase 11: Dashboard Module
### Dashboard Summary Cards
- [ ] Create dashboard page
- [ ] Display pending orders count (API: `/api/dashboard`)
- [ ] Display today's results count
- [ ] Display critical results count
- [ ] Display active patients count
- [ ] Add refresh button
### Orders Trend Chart
- [ ] Create orders over time chart
- [ ] Filter by date range
- [ ] Group by status
- [ ] Use charting library (Chart.js/Recharts/etc.)
### Results Volume Chart
- [ ] Create results volume chart
- [ ] Filter by date range
- [ ] Group by test type or department
- [ ] Use charting library
### Real-time Status Indicators
- [ ] Implement polling or WebSocket for real-time updates
- [ ] Update dashboard metrics automatically
- [ ] Show last update time
- [ ] Add auto-refresh toggle (e.g., every 30 seconds)
### Department Performance
- [ ] Display department-wise metrics
- [ ] Orders processed per department
- [ ] Average turnaround time
- [ ] Results volume per department
**Acceptance Criteria:**
- Dashboard displays all summary metrics
- Charts render correctly
- Real-time updates work
- Filters apply to charts
- Performance metrics are accurate
---
## Phase 12: Edge API Integration
### Instrument Status Monitoring
- [ ] Create instrument status page
- [ ] Display instrument list with status
- [ ] Color-code status (online=green, offline=gray, error=red, maintenance=orange)
- [ ] Implement status polling or WebSocket
- [ ] Display last status update timestamp
- [ ] Display status message (if available)
### Real-time Result Display
- [ ] Implement result streaming (WebSocket or SSE polling)
- [ ] Display incoming results as they arrive
- [ ] Parse EdgeResultRequest format (sample_id, instrument_id, results array)
- [ ] Display results in real-time feed
- [ ] Show test_code, result_value, unit, flags
- [ ] Add sound/notification for new results
### Order Acknowledgment
- [ ] Create edge orders list (API: `/api/edge/orders`)
- [ ] Filter by instrument_id
- [ ] Filter by status (pending, acknowledged)
- [ ] Display orders for instruments
- [ ] Implement acknowledge action (POST `/api/edge/orders/{orderId}/ack`)
- [ ] Update status to acknowledged
- [ ] Show acknowledgment confirmation
### Instrument Status Logging
- [ ] Create status log view
- [ ] Display status history per instrument
- [ ] Show timestamp, status, message
- [ ] Call `/api/edge/status` when receiving status updates
### Result Queue Management
- [ ] Display queued results (EdgeResultResponse: edge_res_id)
- [ ] Show queue status
- [ ] Manual trigger for processing (if needed)
**Acceptance Criteria:**
- Instrument status displays correctly
- Real-time results stream in
- Order acknowledgment works
- Status history is visible
- Result queue is manageable
---
## Phase 13: Testing & QA
### Unit Tests
- [ ] Set up testing framework (Jest, Vitest, etc.)
- [ ] Write unit tests for components
- [ ] Write unit tests for services/API calls
- [ ] Write unit tests for utility functions
- [ ] Write unit tests for state management
- [ ] Achieve >80% code coverage
### Integration Tests
- [ ] Set up API mocking (MSW, Mirage, etc.)
- [ ] Write tests for authentication flow
- [ ] Write tests for patient CRUD operations
- [ ] Write tests for order creation flow
- [ ] Write tests for specimen tracking
- [ ] Write tests for results entry
### E2E Tests (Critical Flows)
- [ ] Set up E2E testing (Cypress, Playwright)
- [ ] Test login → dashboard flow
- [ ] Test patient registration → order creation flow
- [ ] Test order → specimen → results flow
- [ ] Test critical results alerting
- [ ] Test logout
### Form Validation Testing
- [ ] Test all required field validations
- [ ] Test format validations (email, phone, regex)
- [ ] Test duplicate detection
- [ ] Test error message display
### Error Handling Testing
- [ ] Test 401 error handling (redirect to login)
- [ ] Test 404 error handling
- [ ] Test 500 error handling
- [ ] Test network error handling
- [ ] Test timeout handling
### Performance Testing
- [ ] Measure page load times
- [ ] Measure list rendering with large datasets
- [ ] Optimize bundle size (code splitting, lazy loading)
- [ ] Test with slow network (3G)
- [ ] Audit with Lighthouse (score >90)
### Accessibility Audit
- [ ] Test keyboard navigation
- [ ] Test screen reader compatibility
- [ ] Check color contrast ratios
- [ ] Ensure proper ARIA labels
- [ ] Test with accessibility tools (axe, WAVE)
**Acceptance Criteria:**
- All tests pass
- Code coverage >80%
- E2E tests cover critical flows
- Lighthouse score >90
- Accessibility audit passes
---
## Phase 14: Deployment
### Production Build
- [ ] Create production build
- [ ] Optimize assets (minify, compress)
- [ ] Generate source maps
- [ ] Verify build output
- [ ] Check bundle size
### Environment Configuration
- [ ] Set up production environment variables
- [ ] Configure production API base URL
- [ ] Disable development tools
- [ ] Enable error tracking (Sentry, etc.)
- [ ] Configure analytics (if needed)
### API Endpoint Verification
- [ ] Verify all endpoints are accessible
- [ ] Verify CORS configuration
- [ ] Test authentication flow with production backend
- [ ] Test with production data
### Database/Storage (if needed)
- [ ] Configure any client-side storage (IndexedDB, localStorage)
- [ ] Set up data migration scripts (if needed)
- [ ] Test storage operations
### Pre-deployment Checklist
- [ ] Run all tests and ensure they pass
- [ ] Review and merge all PRs
- [ ] Update version number
- [ ] Update CHANGELOG
- [ ] Create release notes
### Deployment
- [ ] Deploy to staging environment
- [ ] Smoke test on staging
- [ ] Deploy to production
- [ ] Verify production deployment
- [ ] Monitor error logs
- [ ] Monitor performance metrics
### Post-deployment
- [ ] Verify critical flows work in production
- [ ] Check user feedback
- [ ] Monitor for errors
- [ ] Rollback plan in case of issues
**Acceptance Criteria:**
- Production build completes without errors
- Application works in production environment
- All critical features verified
- No critical errors in logs
- Performance meets targets
---
## Notes
### API Base URL
- Development: `http://localhost/clqms01/`
- Production: `https://clqms01-api.services-summit.my.id/`
### Authentication
- JWT token stored in HTTP-only cookie
- Bearer token in Authorization header for API calls
- Check auth status on app load
### Common Patterns
- All list pages support pagination
- All forms include validation
- All delete operations require confirmation
- All API calls include error handling
- Loading states for async operations
### Dependencies Between Phases
- Phase 2 (Authentication) must be completed before protected routes
- Phase 3 (Value Sets) needed for dropdowns across all modules - **do this early!**
- Phase 4 (Organization) data needed for filtering in other modules
- Phase 6 (Patients) needed for Phase 9 (Orders)
- Phase 7 (Test Catalog) needed for Phase 9 (Orders)
- Phase 9 (Orders) needed for Phase 10 (Results)
### Testing Priority
- E2E: Login → Dashboard, Patient → Order → Result flow
- Unit: All services, all components with logic
- Integration: API calls, auth flows
---
## Completed Phases Tracker
- [x] Phase 1: Project Setup
- [ ] Phase 2: Authentication Module
- [ ] Phase 3: Value Sets Module
- [ ] Phase 4: Organization Module
- [ ] Phase 5: Master Data Module
- [ ] Phase 6: Patient Management Module
- [ ] Phase 7: Test Catalog Module
- [ ] Phase 8: Specimen Module
- [ ] Phase 9: Orders Module
- [ ] Phase 10: Results Module
- [ ] Phase 11: Dashboard Module
- [ ] Phase 12: Edge API Integration
- [ ] Phase 13: Testing & QA
- [ ] Phase 14: Deployment
---
**Last Updated:** 2026-02-08
**Version:** 1.0.0

108
README.md Normal file
View File

@ -0,0 +1,108 @@
# CLQMS Frontend - Phase 1 Complete
## Project Setup Summary
**Framework:** SvelteKit with TypeScript
**Styling:** Tailwind CSS v4 + DaisyUI v5
**Package Manager:** pnpm
**Deployment:** Static adapter (SPA mode)
## Quick Start
```bash
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
# Preview production build
pnpm preview
```
## Project Structure
```
src/
├── app.css # Global styles with DaisyUI theme
├── app.d.ts # TypeScript declarations
├── app.html # HTML template
├── hooks.server.ts # Server hooks (auth middleware)
├── lib/
│ ├── components/
│ │ ├── index.ts # Component exports
│ │ ├── layout/
│ │ │ ├── Header.svelte # App header with user menu
│ │ │ └── Sidebar.svelte # Navigation sidebar
│ ├── server/
│ │ └── api.ts # API client wrapper
│ ├── stores/
│ │ ├── auth.ts # Auth state management
│ │ └── index.ts # Store exports
│ └── types/ # TypeScript types (to be added)
└── routes/
├── +layout.svelte # Root layout
├── +page.svelte # Landing page (redirects to login)
├── (app)/ # Protected app routes
│ ├── +layout.svelte # App layout with sidebar
│ └── dashboard/
│ └── +page.svelte # Dashboard page
└── (auth)/ # Public auth routes
├── +layout.svelte # Auth layout (no sidebar)
└── login/
└── +page.svelte # Login page
build/ # Production build output
```
## Configuration Files
- `svelte.config.js` - SvelteKit config with static adapter
- `vite.config.ts` - Vite config with Tailwind plugin
- `tsconfig.json` - TypeScript configuration
- `.env` - Environment variables
## Features Implemented
- SvelteKit project initialized with TypeScript
- Tailwind CSS v4 + DaisyUI v5 configured
- Forest theme with medical green colors
- Static adapter for static hosting
- API client with fetch wrapper
- Auth store for state management
- Server hooks for auth middleware
- Login page (converted from template)
- Dashboard page with stats cards
- Sidebar navigation component
- Header component with user menu
- Responsive layout
## Environment Variables
Create a `.env.local` file for local overrides:
```bash
PUBLIC_API_BASE_URL=http://localhost/clqms01/
PUBLIC_API_BASE_URL_PROD=https://clqms01-api.services-summit.my.id/
```
## Next Steps (Phase 2)
1. Implement actual login API integration
2. Add JWT token handling
3. Create protected routes
4. Add logout functionality
5. Implement password change
## Build Output
The production build is generated in the `build/` folder and can be deployed to any static hosting service (Netlify, Vercel, GitHub Pages, etc.).
## Notes
- DaisyUI v5 uses Tailwind v4's new CSS-first configuration
- The forest theme uses medical green (#2d6a4f) as primary color
- Static adapter configured with fallback for SPA routing

2826
api-docs.yaml Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "fe",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.1.18",
"daisyui": "^5.5.18",
"lucide-svelte": "^0.563.0",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"sveltekit-superforms": "^2.29.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"zod": "^4.3.6"
}
}

1744
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

91
src/app.css Normal file
View File

@ -0,0 +1,91 @@
@import "tailwindcss";
/* DaisyUI Import */
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}
/* Custom Theme Configuration */
@plugin "daisyui/theme" {
name: "forest";
default: true;
prefersdark: false;
color-scheme: "light";
/* Primary - Medical Green */
--color-primary: #2d6a4f;
--color-primary-content: #ffffff;
/* Secondary - Light Green */
--color-secondary: #52b788;
--color-secondary-content: #ffffff;
/* Accent */
--color-accent: #d8f3dc;
--color-accent-content: #1b4332;
/* Neutral */
--color-neutral: #40916c;
--color-neutral-content: #ffffff;
/* Base colors */
--color-base-100: #ffffff;
--color-base-200: #f8fafc;
--color-base-300: #e2e8f0;
--color-base-content: #1e293b;
/* Info */
--color-info: #3b82f6;
--color-info-content: #ffffff;
/* Success */
--color-success: #22c55e;
--color-success-content: #ffffff;
/* Warning */
--color-warning: #f59e0b;
--color-warning-content: #ffffff;
/* Error */
--color-error: #ef4444;
--color-error-content: #ffffff;
}
/* Custom utilities */
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* Smooth transitions */
* {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Focus styles */
*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-base-200);
}
::-webkit-scrollbar-thumb {
background: var(--color-primary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral);
}

15
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
interface Locals {
user: { authenticated: boolean } | null;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" data-theme="forest">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

12
src/hooks.server.ts Normal file
View File

@ -0,0 +1,12 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Get auth token from cookies
const authToken = event.cookies.get('auth_token');
// Add auth token to locals for use in load functions
event.locals.user = authToken ? { authenticated: true } : null;
const response = await resolve(event);
return response;
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,2 @@
export { default as Sidebar } from './layout/Sidebar.svelte';
export { default as Header } from './layout/Header.svelte';

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { Bell, Search, Menu } from 'lucide-svelte';
import { auth } from '$stores/auth';
let user: any = null;
auth.subscribe(state => {
user = state.user;
});
</script>
<header class="h-16 bg-base-100 border-b border-base-300 flex items-center justify-between px-6 sticky top-0 z-40">
<div class="flex items-center gap-4">
<button class="btn btn-ghost btn-sm btn-circle lg:hidden">
<Menu class="w-5 h-5" />
</button>
<div class="relative">
<Search class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40" />
<input
type="text"
placeholder="Search..."
class="input input-sm input-bordered w-64 pl-10"
/>
</div>
</div>
<div class="flex items-center gap-4">
<button class="btn btn-ghost btn-circle relative">
<Bell class="w-5 h-5" />
<span class="badge badge-sm badge-error absolute -top-1 -right-1">3</span>
</button>
<div class="flex items-center gap-3">
<div class="text-right hidden sm:block">
<p class="text-sm font-medium">{user?.username || 'User'}</p>
<p class="text-xs text-base-content/60 capitalize">{user?.role || 'User'}</p>
</div>
<div class="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center">
<span class="text-emerald-600 font-semibold">
{user?.username?.charAt(0).toUpperCase() || 'U'}
</span>
</div>
</div>
</div>
</header>

View File

@ -0,0 +1,79 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { auth } from '$stores/auth';
import { LayoutDashboard, Users, FlaskConical, ClipboardList, TestTube, Settings, LogOut } from 'lucide-svelte';
let user: any = null;
auth.subscribe(state => {
user = state.user;
});
function handleLogout() {
auth.logout();
goto('/login');
}
const menuItems = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ name: 'Patients', href: '/patients', icon: Users },
{ name: 'Orders', href: '/orders', icon: ClipboardList },
{ name: 'Tests', href: '/tests', icon: FlaskConical },
{ name: 'Results', href: '/results', icon: TestTube },
{ name: 'Settings', href: '/settings', icon: Settings },
];
</script>
<aside class="fixed left-0 top-0 h-full w-64 bg-base-200 border-r border-base-300 flex flex-col z-50">
<!-- Logo -->
<div class="p-6 border-b border-base-300">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-emerald-100 flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
</svg>
</div>
<div>
<h1 class="text-xl font-bold text-emerald-600">CLQMS</h1>
<p class="text-xs text-base-content/60">Laboratory System</p>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 p-4">
<ul class="menu menu-vertical gap-2">
{#each menuItems as item}
<li>
<a href={item.href} class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-emerald-50 hover:text-emerald-600 transition-colors">
<svelte:component this={item.icon} class="w-5 h-5" />
<span>{item.name}</span>
</a>
</li>
{/each}
</ul>
</nav>
<!-- User Section -->
<div class="p-4 border-t border-base-300">
<div class="flex items-center gap-3 p-3 rounded-lg bg-base-100">
<div class="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center">
<span class="text-emerald-600 font-semibold">
{user?.username?.charAt(0).toUpperCase() || 'U'}
</span>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{user?.username || 'User'}</p>
<p class="text-xs text-base-content/60 capitalize">{user?.role || 'User'}</p>
</div>
<button
on:click={handleLogout}
class="btn btn-ghost btn-sm btn-circle"
title="Logout"
>
<LogOut class="w-4 h-4" />
</button>
</div>
</div>
</aside>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

139
src/lib/server/api.ts Normal file
View File

@ -0,0 +1,139 @@
// API Client for CLQMS
// Wrapper around fetch for making API calls to the backend
import { browser } from '$app/environment';
import { PUBLIC_API_BASE_URL, PUBLIC_API_BASE_URL_PROD } from '$env/static/public';
const API_BASE_URL = browser
? (import.meta.env.DEV ? PUBLIC_API_BASE_URL : PUBLIC_API_BASE_URL_PROD)
: PUBLIC_API_BASE_URL;
interface ApiError {
message: string;
status: number;
code?: string;
}
interface ApiResponse<T> {
data: T | null;
error: ApiError | null;
success: boolean;
}
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string = API_BASE_URL) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}/${endpoint.replace(/^\//, '')}`;
const config: RequestInit = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
},
credentials: 'include'
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
data: null,
error: {
message: errorData.message || `HTTP Error: ${response.status}`,
status: response.status,
code: errorData.code
},
success: false
};
}
const contentType = response.headers.get('content-type');
let data: T;
if (contentType?.includes('application/json')) {
data = await response.json();
} else {
data = await response.text() as unknown as T;
}
return {
data,
error: null,
success: true
};
} catch (error) {
return {
data: null,
error: {
message: error instanceof Error ? error.message : 'Network error',
status: 0
},
success: false
};
}
}
async get<T>(endpoint: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET', headers });
}
async post<T>(
endpoint: string,
body: unknown,
headers?: Record<string, string>
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
headers
});
}
async patch<T>(
endpoint: string,
body: unknown,
headers?: Record<string, string>
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: JSON.stringify(body),
headers
});
}
async put<T>(
endpoint: string,
body: unknown,
headers?: Record<string, string>
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(body),
headers
});
}
async delete<T>(endpoint: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE', headers });
}
}
export const api = new ApiClient();
export type { ApiResponse, ApiError };

47
src/lib/stores/auth.ts Normal file
View File

@ -0,0 +1,47 @@
import { writable } from 'svelte/store';
interface User {
id: string;
username: string;
email?: string;
role?: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
}
function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>({
user: null,
isAuthenticated: false,
isLoading: true
});
return {
subscribe,
login: (user: User) => {
update(state => ({
...state,
user,
isAuthenticated: true,
isLoading: false
}));
},
logout: () => {
set({
user: null,
isAuthenticated: false,
isLoading: false
});
},
setLoading: (loading: boolean) => {
update(state => ({ ...state, isLoading: loading }));
}
};
}
export const auth = createAuthStore();
export type { User, AuthState };

1
src/lib/stores/index.ts Normal file
View File

@ -0,0 +1 @@
export { auth, type User, type AuthState } from './auth';

View File

@ -0,0 +1,15 @@
<script>
import '../../app.css';
import Sidebar from '$components/layout/Sidebar.svelte';
import Header from '$components/layout/Header.svelte';
</script>
<div class="min-h-screen bg-base-100 flex">
<Sidebar />
<div class="flex-1 flex flex-col ml-64">
<Header />
<main class="flex-1 p-6 overflow-auto">
<slot />
</main>
</div>
</div>

View File

@ -0,0 +1,184 @@
<script lang="ts">
import { onMount } from 'svelte';
import { auth } from '$stores/auth';
import {
Users,
ClipboardList,
TestTube,
AlertCircle,
TrendingUp,
Calendar
} from 'lucide-svelte';
let stats = {
totalPatients: 1234,
pendingOrders: 45,
todayResults: 128,
criticalResults: 3
};
onMount(() => {
auth.setLoading(false);
});
</script>
<svelte:head>
<title>CLQMS - Dashboard</title>
</svelte:head>
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-base-content">Dashboard</h1>
<p class="text-base-content/60 flex items-center gap-2 mt-1">
<Calendar class="w-4 h-4" />
{new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</p>
</div>
<button class="btn btn-primary gap-2">
<TrendingUp class="w-4 h-4" />
View Reports
</button>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Patients -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Total Patients</p>
<p class="text-3xl font-bold text-emerald-600">{stats.totalPatients}</p>
</div>
<div class="w-12 h-12 rounded-xl bg-emerald-100 flex items-center justify-center">
<Users class="w-6 h-6 text-emerald-600" />
</div>
</div>
<div class="mt-4 flex items-center text-sm text-success">
<TrendingUp class="w-4 h-4 mr-1" />
<span>+12% from last month</span>
</div>
</div>
</div>
<!-- Pending Orders -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Pending Orders</p>
<p class="text-3xl font-bold text-warning">{stats.pendingOrders}</p>
</div>
<div class="w-12 h-12 rounded-xl bg-warning/10 flex items-center justify-center">
<ClipboardList class="w-6 h-6 text-warning" />
</div>
</div>
<div class="mt-4">
<span class="text-sm text-base-content/60">Requires attention</span>
</div>
</div>
</div>
<!-- Today's Results -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Today's Results</p>
<p class="text-3xl font-bold text-info">{stats.todayResults}</p>
</div>
<div class="w-12 h-12 rounded-xl bg-info/10 flex items-center justify-center">
<TestTube class="w-6 h-6 text-info" />
</div>
</div>
<div class="mt-4">
<span class="text-sm text-base-content/60">Processed today</span>
</div>
</div>
</div>
<!-- Critical Results -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Critical Results</p>
<p class="text-3xl font-bold text-error">{stats.criticalResults}</p>
</div>
<div class="w-12 h-12 rounded-xl bg-error/10 flex items-center justify-center">
<AlertCircle class="w-6 h-6 text-error" />
</div>
</div>
<div class="mt-4">
<span class="text-sm text-error font-medium">Immediate attention required</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Quick Actions</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<a href="/patients/new" class="btn btn-outline btn-primary h-auto py-4 flex flex-col gap-2">
<Users class="w-6 h-6" />
<span>New Patient</span>
</a>
<a href="/orders/new" class="btn btn-outline btn-primary h-auto py-4 flex flex-col gap-2">
<ClipboardList class="w-6 h-6" />
<span>New Order</span>
</a>
<a href="/results" class="btn btn-outline btn-primary h-auto py-4 flex flex-col gap-2">
<TestTube class="w-6 h-6" />
<span>View Results</span>
</a>
<a href="/settings" class="btn btn-outline btn-primary h-auto py-4 flex flex-col gap-2">
<span class="text-2xl">⚙️</span>
<span>Settings</span>
</a>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="card bg-base-100 shadow-lg border border-base-200">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Recent Activity</h2>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Time</th>
<th>Action</th>
<th>User</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>10:30 AM</td>
<td>New patient registered</td>
<td>Dr. Smith</td>
<td><span class="badge badge-success">Completed</span></td>
</tr>
<tr>
<td>10:15 AM</td>
<td>Lab order created</td>
<td>Nurse Johnson</td>
<td><span class="badge badge-warning">Pending</span></td>
</tr>
<tr>
<td>09:45 AM</td>
<td>Results uploaded</td>
<td>Lab Tech Mike</td>
<td><span class="badge badge-success">Completed</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
<script>
import '../../app.css';
</script>
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20">
<slot />
</div>

View File

@ -0,0 +1,162 @@
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
import { auth } from '$stores/auth';
import { goto } from '$app/navigation';
import { User, Lock, LogIn } from 'lucide-svelte';
const loginSchema = z.object({
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
remember: z.boolean().default(false)
});
type LoginForm = z.infer<typeof loginSchema>;
let loading = false;
let errorMessage = '';
async function handleSubmit(event: SubmitEvent) {
event.preventDefault();
loading = true;
errorMessage = '';
const formData = new FormData(event.target as HTMLFormElement);
const username = formData.get('username') as string;
const password = formData.get('password') as string;
try {
// TODO: Replace with actual API call
// const response = await api.post('/api/auth/login', { username, password });
// Simulate successful login
await new Promise(resolve => setTimeout(resolve, 1000));
auth.login({
id: '1',
username: username,
role: 'admin'
});
goto('/dashboard');
} catch (error) {
errorMessage = 'Invalid username or password';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>CLQMS - Login</title>
</svelte:head>
<div class="hero min-h-screen">
<div class="hero-content flex-col">
<!-- Logo Section -->
<div class="text-center mb-6">
<div class="w-20 h-20 mx-auto rounded-2xl bg-emerald-100 flex items-center justify-center mb-4 shadow-lg border-2 border-emerald-200">
<svg class="w-12 h-12 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
</svg>
</div>
<h1 class="text-4xl font-bold text-emerald-600 mb-2">CLQMS</h1>
<p class="text-base-content/60">Clinical Laboratory Quality Management System</p>
</div>
<!-- Login Card -->
<div class="card bg-base-100 w-full max-w-sm shadow-2xl border-t-4 border-emerald-500">
<div class="card-body p-8">
<h2 class="text-2xl font-bold text-center text-emerald-700 mb-6">Welcome Back</h2>
{#if errorMessage}
<div class="alert alert-error mb-4">
<span>{errorMessage}</span>
</div>
{/if}
<form on:submit={handleSubmit}>
<!-- Username Field -->
<div class="form-control mb-4">
<label class="label" for="username">
<span class="label-text font-medium">Username</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User class="w-5 h-5 text-emerald-500" />
</span>
<input
type="text"
id="username"
name="username"
placeholder="Enter your username"
class="input input-bordered w-full pl-10 focus:border-emerald-500 focus:ring-emerald-500"
required
/>
</div>
</div>
<!-- Password Field -->
<div class="form-control mb-4">
<label class="label" for="password">
<span class="label-text font-medium">Password</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock class="w-5 h-5 text-emerald-500" />
</span>
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
class="input input-bordered w-full pl-10 focus:border-emerald-500 focus:ring-emerald-500"
required
/>
</div>
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between mb-6">
<label class="label cursor-pointer flex items-center gap-2">
<input type="checkbox" name="remember" class="checkbox checkbox-sm checkbox-primary" />
<span class="label-text text-sm">Remember me</span>
</label>
<a href="#" class="text-sm text-emerald-600 hover:text-emerald-700 hover:underline">Forgot password?</a>
</div>
<!-- Login Button -->
<div class="form-control">
<button
type="submit"
class="btn bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg hover:shadow-xl transition-all"
disabled={loading}
>
{#if loading}
<span class="loading loading-spinner loading-sm mr-2"></span>
Signing in...
{:else}
<LogIn class="w-5 h-5 mr-2" />
Sign In
{/if}
</button>
</div>
</form>
<!-- Divider -->
<div class="divider text-sm text-base-content/40">or</div>
<!-- Help Text -->
<p class="text-center text-sm text-base-content/60">
Need help? Contact your system administrator
</p>
</div>
</div>
<!-- Footer -->
<div class="mt-8 text-center">
<p class="text-sm text-base-content/40">2024 CLQMS. All rights reserved.</p>
</div>
</div>
</div>

12
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,12 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
import '../app.css';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children()}

19
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,19 @@
<script>
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
goto('/login');
});
</script>
<svelte:head>
<meta http-equiv="refresh" content="0;url=/login">
</svelte:head>
<div class="min-h-screen flex items-center justify-center bg-base-100">
<div class="text-center">
<div class="loading loading-spinner loading-lg text-primary mb-4"></div>
<p class="text-base-content/60">Redirecting...</p>
</div>
</div>

3
static/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

22
svelte.config.js Normal file
View File

@ -0,0 +1,22 @@
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: false,
strict: false
}),
alias: {
'$components': 'src/lib/components',
'$stores': 'src/lib/stores',
'$types': 'src/lib/types',
'$server': 'src/lib/server'
}
}
};
export default config;

297
templates/dashboard.html Normal file
View File

@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS - Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="shared.css">
<style>
/* Mobile: drawer overlay behavior */
@media (max-width: 1023px) {
.sidebar-container {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 40;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .sidebar-container {
transform: translateX(0);
}
#sidebar-toggle:checked ~ .main-content {
margin-left: 0;
}
.overlay {
display: block;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#sidebar-toggle:checked ~ .overlay {
opacity: 1;
visibility: visible;
}
}
/* Desktop: slide and push behavior */
@media (min-width: 1024px) {
.page-wrapper {
display: flex;
}
.sidebar-container {
position: relative;
width: 0;
flex-shrink: 0;
overflow: hidden;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .page-wrapper .sidebar-container {
width: 320px;
}
.main-content {
flex: 1;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.overlay {
display: none;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20">
<input id="sidebar-toggle" type="checkbox" class="hidden" />
<label for="sidebar-toggle" class="overlay"></label>
<div class="page-wrapper min-h-screen">
<!-- Sidebar -->
<div class="sidebar-container">
<div class="h-full w-80 bg-base-200 shadow-xl border-r border-base-300">
<div class="p-4">
<div class="flex items-center gap-3 px-2 mb-6">
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
</div>
<span class="text-xl font-bold text-gray-800">CLQMS</span>
</div>
<ul class="menu w-full">
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-2">Main</li>
<li><a href="dashboard.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700 active:bg-emerald-100">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a></li>
<li><a href="master-data.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
Master Data
</a></li>
<li><a href="result-entry.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Result Entry
</a></li>
<li><a href="report-print.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
Reports
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Laboratory</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Patients
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
Orders
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
Specimens
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Results
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Administration</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organization
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Users
</a></li>
<li class="mt-4 pt-4 border-t border-gray-200"><a href="login.html" class="text-red-500 hover:bg-red-50">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content flex flex-col">
<!-- Header -->
<div class="navbar bg-base-100 shadow-sm border-b border-base-300">
<div class="flex-none">
<label for="sidebar-toggle" class="btn btn-square btn-ghost text-gray-600 hover:bg-emerald-50 hover:text-emerald-600 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl font-bold text-gray-800 hover:bg-gray-100">
<svg class="w-6 h-6 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
CLQMS Dashboard
</a>
</div>
<div class="flex-none gap-2">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder hover:bg-emerald-50">
<div class="bg-emerald-100 text-emerald-700 rounded-full w-10 border-2 border-emerald-200 flex items-center justify-center">
<span class="font-semibold">DL</span>
</div>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-emerald-200">
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>Profile</a></li>
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>Settings</a></li>
<div class="divider my-0"></div>
<li><a href="login.html" class="text-error hover:bg-red-50"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>Logout</a></li>
</ul>
</div>
</div>
</div>
<!-- Dashboard Content -->
<main class="flex-1 overflow-auto p-6">
<!-- Summary Stats -->
<div class="stats stats-vertical lg:stats-horizontal shadow-xl w-full mb-6 bg-base-100/80 backdrop-blur">
<div class="stat border-l-4 border-emerald-500 hover:bg-emerald-50/50 transition-colors">
<div class="stat-figure text-emerald-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div class="stat-title text-emerald-700">Pending Orders</div>
<div class="stat-value text-emerald-600">24</div>
<div class="stat-desc text-emerald-600/70">Jan 1 - Feb 8</div>
</div>
<div class="stat border-l-4 border-green-500 hover:bg-green-50/50 transition-colors">
<div class="stat-figure text-green-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div class="stat-title text-green-700">Today's Results</div>
<div class="stat-value text-green-600">156</div>
<div class="stat-desc text-green-600/70">↗︎ 14% more than yesterday</div>
</div>
<div class="stat border-l-4 border-red-500 hover:bg-red-50/50 transition-colors">
<div class="stat-figure text-red-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<div class="stat-title text-red-700">Critical Results</div>
<div class="stat-value text-red-600">3</div>
<div class="stat-desc text-red-600/70">Requires attention</div>
</div>
<div class="stat border-l-4 border-teal-500 hover:bg-teal-50/50 transition-colors">
<div class="stat-figure text-teal-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<div class="stat-title text-teal-700">Active Patients</div>
<div class="stat-value text-teal-600">89</div>
<div class="stat-desc text-teal-600/70">Currently in system</div>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="card bg-base-100 shadow-xl border-t-4 border-emerald-500 hover:shadow-2xl transition-shadow">
<div class="card-body">
<h2 class="card-title text-emerald-700">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
Orders Trend
</h2>
<div class="h-64 flex items-center justify-center bg-gradient-to-br from-emerald-50 to-teal-50 rounded-lg border border-emerald-100">
<p class="text-emerald-600/60">[Chart: Orders over time]</p>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl border-t-4 border-teal-500 hover:shadow-2xl transition-shadow">
<div class="card-body">
<h2 class="card-title text-teal-700">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/></svg>
Results Volume
</h2>
<div class="h-64 flex items-center justify-center bg-gradient-to-br from-teal-50 to-cyan-50 rounded-lg border border-teal-100">
<p class="text-teal-600/60">[Chart: Results by department]</p>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="card bg-base-100 shadow-xl border-t-4 border-green-500 hover:shadow-2xl transition-shadow">
<div class="card-body">
<h2 class="card-title mb-4 text-green-700">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Recent Activity
</h2>
<ul class="timeline timeline-vertical">
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-emerald-100 flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-emerald-50 to-white border-l-4 border-emerald-500">
<time class="text-sm font-mono text-emerald-600">09:30 AM</time>
<div class="text-lg font-black text-emerald-800">Order #12345 created</div>
<div class="text-sm text-emerald-600/80">Patient: John Doe (P-1001)</div>
</div>
<hr class="bg-emerald-300"/>
</li>
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-green-50 to-white border-l-4 border-green-500">
<time class="text-sm font-mono text-green-600">09:15 AM</time>
<div class="text-lg font-black text-green-800">Result received</div>
<div class="text-sm text-green-600/80">Sample: ABC123 - Instrument: CBC-M01</div>
</div>
<hr class="bg-green-300"/>
</li>
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-teal-100 flex items-center justify-center">
<svg class="w-4 h-4 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-teal-50 to-white border-l-4 border-teal-500">
<time class="text-sm font-mono text-teal-600">09:00 AM</time>
<div class="text-lg font-black text-teal-800">Patient registered</div>
<div class="text-sm text-teal-600/80">Patient ID: P-1001 - Jane Smith</div>
</div>
</li>
</ul>
</div>
</div>
</main>
</div>
</div>
</body>
</html>

98
templates/login.html Normal file
View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS - Login</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="shared.css">
</head>
<body class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20 flex items-center justify-center">
<div class="hero min-h-screen">
<div class="hero-content flex-col">
<!-- Logo Section -->
<div class="text-center mb-6">
<div class="w-20 h-20 mx-auto rounded-2xl bg-emerald-100 flex items-center justify-center mb-4 shadow-lg border-2 border-emerald-200">
<svg class="w-12 h-12 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
</svg>
</div>
<h1 class="text-4xl font-bold text-emerald-600 mb-2">CLQMS</h1>
<p class="text-base-content/60">Clinical Laboratory Quality Management System</p>
</div>
<!-- Login Card -->
<div class="card bg-base-100 w-full max-w-sm shadow-2xl border-t-4 border-emerald-500">
<div class="card-body p-8">
<h2 class="text-2xl font-bold text-center text-emerald-700 mb-6">Welcome Back</h2>
<form>
<!-- Username Field -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text font-medium">Username</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</span>
<input type="text" placeholder="Enter your username" class="input input-bordered w-full pl-10 focus:border-emerald-500 focus:ring-emerald-500" />
</div>
</div>
<!-- Password Field -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text font-medium">Password</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</span>
<input type="password" placeholder="Enter your password" class="input input-bordered w-full pl-10 focus:border-emerald-500 focus:ring-emerald-500" />
</div>
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between mb-6">
<label class="label cursor-pointer flex items-center gap-2">
<input type="checkbox" class="checkbox checkbox-sm checkbox-emerald" />
<span class="label-text text-sm">Remember me</span>
</label>
<a href="#" class="text-sm text-emerald-600 hover:text-emerald-700 hover:underline">Forgot password?</a>
</div>
<!-- Login Button -->
<div class="form-control">
<button class="btn bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg hover:shadow-xl transition-all">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"/>
</svg>
Sign In
</button>
</div>
</form>
<!-- Divider -->
<div class="divider text-sm text-base-content/40">or</div>
<!-- Help Text -->
<p class="text-center text-sm text-base-content/60">
Need help? Contact your system administrator
</p>
</div>
</div>
<!-- Footer -->
<div class="mt-8 text-center">
<p class="text-sm text-base-content/40">© 2024 CLQMS. All rights reserved.</p>
</div>
</div>
</div>
</body>
</html>

325
templates/master-data.html Normal file
View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS - Master Data - Locations</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="shared.css">
<style>
/* Mobile: drawer overlay behavior */
@media (max-width: 1023px) {
.sidebar-container {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 40;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .sidebar-container {
transform: translateX(0);
}
#sidebar-toggle:checked ~ .main-content {
margin-left: 0;
}
.overlay {
display: block;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#sidebar-toggle:checked ~ .overlay {
opacity: 1;
visibility: visible;
}
}
/* Desktop: slide and push behavior */
@media (min-width: 1024px) {
.page-wrapper {
display: flex;
}
.sidebar-container {
position: relative;
width: 0;
flex-shrink: 0;
overflow: hidden;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .page-wrapper .sidebar-container {
width: 320px;
}
.main-content {
flex: 1;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.overlay {
display: none;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20">
<input id="sidebar-toggle" type="checkbox" class="hidden" />
<label for="sidebar-toggle" class="overlay"></label>
<div class="page-wrapper min-h-screen">
<!-- Sidebar -->
<div class="sidebar-container">
<div class="h-full w-80 bg-base-200 shadow-xl border-r border-base-300">
<div class="p-4">
<div class="flex items-center gap-3 px-2 mb-6">
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
</div>
<span class="text-xl font-bold text-gray-800">CLQMS</span>
</div>
<ul class="menu w-full">
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-2">Main</li>
<li><a href="dashboard.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a></li>
<li><a href="master-data.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700 active bg-emerald-100">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
Master Data
</a></li>
<li><a href="result-entry.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Result Entry
</a></li>
<li><a href="report-print.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
Reports
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Laboratory</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Patients
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
Orders
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
Specimens
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Results
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Administration</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organization
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Users
</a></li>
<li class="mt-4 pt-4 border-t border-gray-200"><a href="login.html" class="text-red-500 hover:bg-red-50">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content flex flex-col">
<!-- Header -->
<div class="navbar bg-base-100 shadow-sm border-b border-base-300">
<div class="flex-none">
<label for="sidebar-toggle" class="btn btn-square btn-ghost text-gray-600 hover:bg-emerald-50 hover:text-emerald-600 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl font-bold text-gray-800 hover:bg-gray-100">
<svg class="w-6 h-6 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
Master Data - Locations
</a>
</div>
<div class="flex-none gap-2">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder hover:bg-emerald-50">
<div class="bg-emerald-100 text-emerald-700 rounded-full w-10 border-2 border-emerald-200 flex items-center justify-center">
<span class="font-semibold">DL</span>
</div>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-emerald-200">
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>Profile</a></li>
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>Settings</a></li>
<div class="divider my-0"></div>
<li><a href="login.html" class="text-error hover:bg-red-50"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>Logout</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Content Area -->
<main class="flex-1 overflow-auto p-6">
<!-- Toolbar -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex flex-col lg:flex-row gap-4 justify-between">
<div class="join w-full lg:w-auto">
<input type="text" placeholder="Search locations..." class="input input-bordered join-item" />
<select class="select select-bordered join-item">
<option>All Types</option>
<option>Ward</option>
<option>Clinic</option>
<option>Laboratory</option>
<option>Pharmacy</option>
</select>
</div>
<button class="btn btn-primary" onclick="document.getElementById('location_modal').showModal()">Add New Location</button>
</div>
</div>
</div>
<!-- Data Table -->
<div class="card bg-base-100 shadow-xl">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Location Code</th>
<th>Location Name</th>
<th>Location Type</th>
<th>Site</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<th>LOC-001</th>
<td>Main Ward</td>
<td><div class="badge badge-ghost">Ward</div></td>
<td>Site A</td>
<td class="text-right">
<div class="join justify-end">
<button class="btn btn-sm btn-ghost">Edit</button>
<button class="btn btn-sm btn-error btn-ghost">Delete</button>
</div>
</td>
</tr>
<tr>
<th>LOC-002</th>
<td>Clinic A</td>
<td><div class="badge badge-info">Clinic</div></td>
<td>Site A</td>
<td class="text-right">
<div class="join justify-end">
<button class="btn btn-sm btn-ghost">Edit</button>
<button class="btn btn-sm btn-error btn-ghost">Delete</button>
</div>
</td>
</tr>
<tr>
<th>LOC-003</th>
<td>Emergency Lab</td>
<td><div class="badge badge-success">Laboratory</div></td>
<td>Site B</td>
<td class="text-right">
<div class="join justify-end">
<button class="btn btn-sm btn-ghost">Edit</button>
<button class="btn btn-sm btn-error btn-ghost">Delete</button>
</div>
</td>
</tr>
<tr>
<th>LOC-004</th>
<td>Main Pharmacy</td>
<td><div class="badge badge-accent">Pharmacy</div></td>
<td>Site A</td>
<td class="text-right">
<div class="join justify-end">
<button class="btn btn-sm btn-ghost">Edit</button>
<button class="btn btn-sm btn-error btn-ghost">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="join justify-center mt-4">
<button class="join-item btn btn-sm" disabled>Previous</button>
<button class="join-item btn btn-sm btn-active">1</button>
<button class="join-item btn btn-sm">2</button>
<button class="join-item btn btn-sm">3</button>
<button class="join-item btn btn-sm">...</button>
<button class="join-item btn btn-sm">10</button>
<button class="join-item btn btn-sm">Next</button>
</div>
</div>
</main>
</div>
</div>
<!-- Add/Edit Modal -->
<dialog id="location_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg text-primary mb-4">Add Location</h3>
<form>
<div class="form-control">
<label class="label"><span class="label-text">Location Code</span></label>
<input type="text" placeholder="e.g., LOC-001" class="input input-bordered" />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Location Name</span></label>
<input type="text" placeholder="e.g., Main Ward" class="input input-bordered" />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Location Type</span></label>
<select class="select select-bordered">
<option>Ward</option>
<option>Clinic</option>
<option>Laboratory</option>
<option>Pharmacy</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Site</span></label>
<select class="select select-bordered">
<option>Site A</option>
<option>Site B</option>
<option>Site C</option>
</select>
</div>
</form>
<div class="modal-action">
<form method="dialog">
<button class="btn">Cancel</button>
</form>
<button class="btn btn-primary">Save</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</body>
</html>

331
templates/report-print.html Normal file
View File

@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS - Results Report</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="shared.css">
<style>
/* Mobile: drawer overlay behavior */
@media (max-width: 1023px) {
.sidebar-container {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 40;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .sidebar-container {
transform: translateX(0);
}
#sidebar-toggle:checked ~ .main-content {
margin-left: 0;
}
.overlay {
display: block;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#sidebar-toggle:checked ~ .overlay {
opacity: 1;
visibility: visible;
}
}
/* Desktop: slide and push behavior */
@media (min-width: 1024px) {
.page-wrapper {
display: flex;
}
.sidebar-container {
position: relative;
width: 0;
flex-shrink: 0;
overflow: hidden;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .page-wrapper .sidebar-container {
width: 320px;
}
.main-content {
flex: 1;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.overlay {
display: none;
}
}
/* Print styles */
@media print {
.no-print { display: none !important; }
.sidebar-container { display: none !important; }
.navbar { display: none !important; }
body { background: white; }
}
</style>
</head>
<body class="bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20">
<input id="sidebar-toggle" type="checkbox" class="hidden" />
<label for="sidebar-toggle" class="overlay"></label>
<div class="page-wrapper min-h-screen">
<!-- Sidebar -->
<div class="sidebar-container no-print">
<div class="h-full w-80 bg-base-200 shadow-xl border-r border-base-300">
<div class="p-4">
<div class="flex items-center gap-3 px-2 mb-6">
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
</div>
<span class="text-xl font-bold text-gray-800">CLQMS</span>
</div>
<ul class="menu w-full">
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-2">Main</li>
<li><a href="dashboard.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700 active:bg-emerald-100">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a></li>
<li><a href="master-data.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
Master Data
</a></li>
<li><a href="result-entry.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Result Entry
</a></li>
<li><a href="report-print.html" class="active text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
Reports
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Laboratory</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Patients
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
Orders
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
Specimens
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Results
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Administration</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organization
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Users
</a></li>
<li class="mt-4 pt-4 border-t border-gray-200"><a href="login.html" class="text-red-500 hover:bg-red-50">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content flex flex-col">
<!-- Header -->
<div class="navbar bg-base-100 shadow-sm border-b border-base-300 no-print">
<div class="flex-none">
<label for="sidebar-toggle" class="btn btn-square btn-ghost text-gray-600 hover:bg-emerald-50 hover:text-emerald-600 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl font-bold text-gray-800 hover:bg-gray-100">
<svg class="w-6 h-6 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
Results Report
</a>
</div>
<div class="flex-none gap-2 no-print">
<button onclick="window.print()" class="btn btn-primary">Print Report</button>
<button class="btn btn-ghost ml-2">Back to Entry</button>
</div>
<div class="flex-none gap-2 no-print">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder hover:bg-emerald-50">
<div class="bg-emerald-100 text-emerald-700 rounded-full w-10 border-2 border-emerald-200 flex items-center justify-center">
<span class="font-semibold">DL</span>
</div>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-emerald-200">
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>Profile</a></li>
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>Settings</a></li>
<div class="divider my-0"></div>
<li><a href="login.html" class="text-error hover:bg-red-50"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>Logout</a></li>
</ul>
</div>
</div>
</div>
<!-- Report Content -->
<main class="flex-1 overflow-auto p-6">
<div class="mockup-window bg-base-100 shadow-2xl max-w-4xl mx-auto">
<div class="p-8">
<!-- Report Header -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-primary mb-2">CLQMS</h1>
<p class="text-lg text-base-content/70">Clinical Laboratory Quality Management System</p>
<div class="divider"></div>
<h2 class="text-2xl font-bold">Laboratory Results Report</h2>
</div>
<!-- Patient Information -->
<div class="mb-6">
<h3 class="font-bold text-lg text-primary mb-3">Patient Information</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<span class="text-sm text-base-content/50">Patient Name:</span>
<span class="ml-2 font-bold">John Doe</span>
</div>
<div>
<span class="text-sm text-base-content/50">Patient ID:</span>
<span class="ml-2 font-bold">P-1001</span>
</div>
<div>
<span class="text-sm text-base-content/50">Date of Birth:</span>
<span class="ml-2">1985-03-15</span>
</div>
<div>
<span class="text-sm text-base-content/50">Sex:</span>
<span class="ml-2">Male</span>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Order Information -->
<div class="mb-6">
<h3 class="font-bold text-lg text-primary mb-3">Order Information</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<span class="text-sm text-base-content/50">Order ID:</span>
<span class="ml-2 font-bold">ORD-2024001</span>
</div>
<div>
<span class="text-sm text-base-content/50">Order Date:</span>
<span class="ml-2">2024-02-08 10:30</span>
</div>
<div>
<span class="text-sm text-base-content/50">Priority:</span>
<div class="badge badge-success ml-2">Routine</div>
</div>
<div>
<span class="text-sm text-base-content/50">Requesting Physician:</span>
<span class="ml-2">Dr. Smith</span>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Results Table -->
<div class="mb-6">
<h3 class="font-bold text-lg text-primary mb-3">Test Results</h3>
<table class="table">
<thead>
<tr>
<th>Test Code</th>
<th>Test Name</th>
<th>Result</th>
<th>Unit</th>
<th>Flag</th>
<th>Reference Range</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal">CBC-HGB</th>
<td>Hemoglobin</td>
<td class="font-bold">145</td>
<td>g/L</td>
<td><div class="badge badge-success">N</div></td>
<td>130-170</td>
</tr>
<tr class="bg-error/10">
<th class="font-normal">CBC-WBC</th>
<td>White Blood Cell Count</td>
<td class="font-bold text-error">12.5</td>
<td>10^9/L</td>
<td><div class="badge badge-error">H</div></td>
<td>4.0-11.0</td>
</tr>
<tr>
<th class="font-normal">CBC-RBC</th>
<td>Red Blood Cell Count</td>
<td class="font-bold">4.8</td>
<td>10^12/L</td>
<td><div class="badge badge-success">N</div></td>
<td>4.5-5.5</td>
</tr>
<tr>
<th class="font-normal">CBC-PLT</th>
<td>Platelet Count</td>
<td class="font-bold">250</td>
<td>10^9/L</td>
<td><div class="badge badge-success">N</div></td>
<td>150-400</td>
</tr>
<tr>
<th class="font-normal">CBC-HCT</th>
<td>Hematocrit</td>
<td class="font-bold">42</td>
<td>%</td>
<td><div class="badge badge-success">N</div></td>
<td>40-50</td>
</tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<!-- Footer Info -->
<div class="text-sm text-base-content/70 mb-4">
<p><strong>Report Generated:</strong> 2024-02-08 14:30:00</p>
<p><strong>Verified By:</strong> Dr. Lab User</p>
</div>
<div class="alert alert-info">
<svg class="w-6 h-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span><strong>Interpretation:</strong> Results show elevated WBC count, may indicate infection. Follow-up recommended.</span>
</div>
</div>
</div>
</main>
</div>
</div>
</body>
</html>

318
templates/result-entry.html Normal file
View File

@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS - Result Entry</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="shared.css">
<style>
/* Mobile: drawer overlay behavior */
@media (max-width: 1023px) {
.sidebar-container {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 40;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .sidebar-container {
transform: translateX(0);
}
#sidebar-toggle:checked ~ .main-content {
margin-left: 0;
}
.overlay {
display: block;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#sidebar-toggle:checked ~ .overlay {
opacity: 1;
visibility: visible;
}
}
/* Desktop: slide and push behavior */
@media (min-width: 1024px) {
.page-wrapper {
display: flex;
}
.sidebar-container {
position: relative;
width: 0;
flex-shrink: 0;
overflow: hidden;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar-toggle:checked ~ .page-wrapper .sidebar-container {
width: 320px;
}
.main-content {
flex: 1;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.overlay {
display: none;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-base-200 via-base-100 to-emerald-50/20">
<input id="sidebar-toggle" type="checkbox" class="hidden" />
<label for="sidebar-toggle" class="overlay"></label>
<div class="page-wrapper min-h-screen">
<!-- Sidebar -->
<div class="sidebar-container">
<div class="h-full w-80 bg-base-200 shadow-xl border-r border-base-300">
<div class="p-4">
<div class="flex items-center gap-3 px-2 mb-6">
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
</div>
<span class="text-xl font-bold text-gray-800">CLQMS</span>
</div>
<ul class="menu w-full">
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-2">Main</li>
<li><a href="dashboard.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700 active:bg-emerald-100">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a></li>
<li><a href="master-data.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
Master Data
</a></li>
<li><a href="result-entry.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700 bg-emerald-50 text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Result Entry
</a></li>
<li><a href="report-print.html" class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
Reports
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Laboratory</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Patients
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
Orders
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
Specimens
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Results
</a></li>
<li class="menu-title uppercase font-bold text-xs text-emerald-600/70 mt-4">Administration</li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organization
</a></li>
<li><a class="text-gray-700 hover:bg-emerald-50 hover:text-emerald-700">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Users
</a></li>
<li class="mt-4 pt-4 border-t border-gray-200"><a href="login.html" class="text-red-500 hover:bg-red-50">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a></li>
</ul>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content flex flex-col">
<!-- Header -->
<div class="navbar bg-base-100 shadow-sm border-b border-base-300">
<div class="flex-none">
<label for="sidebar-toggle" class="btn btn-square btn-ghost text-gray-600 hover:bg-emerald-50 hover:text-emerald-600 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl font-bold text-gray-800 hover:bg-gray-100">
<svg class="w-6 h-6 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Result Entry
</a>
</div>
<div class="flex-none gap-2">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder hover:bg-emerald-50">
<div class="bg-emerald-100 text-emerald-700 rounded-full w-10 border-2 border-emerald-200 flex items-center justify-center">
<span class="font-semibold">DL</span>
</div>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-emerald-200">
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>Profile</a></li>
<li><a class="hover:bg-emerald-50"><svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>Settings</a></li>
<div class="divider my-0"></div>
<li><a href="login.html" class="text-error hover:bg-red-50"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>Logout</a></li>
</ul>
</div>
</div>
</div>
<!-- Result Entry Content -->
<main class="flex-1 overflow-auto p-6">
<!-- Order Selection -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h3 class="font-bold text-lg text-primary mb-4">Select Order</h3>
<div class="flex flex-col lg:flex-row gap-4">
<div class="form-control flex-1">
<label class="label"><span class="label-text">Order ID</span></label>
<input type="text" placeholder="Enter order ID" class="input input-bordered" />
</div>
<div class="form-control flex-1">
<label class="label"><span class="label-text">Patient ID</span></label>
<input type="text" placeholder="Enter patient ID" class="input input-bordered" />
</div>
<div class="form-control flex-none">
<label class="label">&nbsp;</label>
<button class="btn btn-primary">Search</button>
</div>
</div>
</div>
</div>
<!-- Patient & Order Info -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h3 class="font-bold text-lg text-primary mb-4">Order Information</h3>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Patient Name</span></label>
<input type="text" value="John Doe" class="input input-bordered input-disabled" disabled />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Patient ID</span></label>
<input type="text" value="P-1001" class="input input-bordered input-disabled" disabled />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Order ID</span></label>
<input type="text" value="ORD-2024001" class="input input-bordered input-disabled" disabled />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Priority</span></label>
<div class="badge badge-success">Routine</div>
</div>
</div>
</div>
</div>
<!-- Results Entry Form -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg text-primary">Test Results</h3>
<div role="alert" class="alert alert-warning">
<svg class="w-6 h-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span>1 critical result detected</span>
</div>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Test Code</th>
<th>Test Name</th>
<th>Result Value</th>
<th>Unit</th>
<th>Flag</th>
<th>Reference Range</th>
</tr>
</thead>
<tbody>
<tr class="hover">
<th class="font-normal">CBC-HGB</th>
<td>Hemoglobin</td>
<td>
<input type="text" value="145" class="input input-bordered input-sm w-full" />
</td>
<td>g/L</td>
<td><div class="badge badge-success">N</div></td>
<td class="text-sm">130-170</td>
</tr>
<tr class="hover">
<th class="font-normal">CBC-WBC</th>
<td>White Blood Cell Count</td>
<td>
<input type="text" value="12.5" class="input input-bordered input-sm w-full input-error" />
</td>
<td>10^9/L</td>
<td><div class="badge badge-error">H</div></td>
<td class="text-sm">4.0-11.0</td>
</tr>
<tr class="hover">
<th class="font-normal">CBC-RBC</th>
<td>Red Blood Cell Count</td>
<td>
<input type="text" placeholder="Enter value" class="input input-bordered input-sm w-full" />
</td>
<td>10^12/L</td>
<td><div class="badge badge-ghost">-</div></td>
<td class="text-sm">4.5-5.5</td>
</tr>
<tr class="hover">
<th class="font-normal">CBC-PLT</th>
<td>Platelet Count</td>
<td>
<input type="text" placeholder="Enter value" class="input input-bordered input-sm w-full" />
</td>
<td>10^9/L</td>
<td><div class="badge badge-ghost">-</div></td>
<td class="text-sm">150-400</td>
</tr>
<tr class="hover">
<th class="font-normal">CBC-HCT</th>
<td>Hematocrit</td>
<td>
<input type="text" placeholder="Enter value" class="input input-bordered input-sm w-full" />
</td>
<td>%</td>
<td><div class="badge badge-ghost">-</div></td>
<td class="text-sm">40-50</td>
</tr>
</tbody>
</table>
</div>
<!-- Form Actions -->
<div class="flex flex-col lg:flex-row gap-4 justify-end mt-6">
<button class="btn btn-ghost">Clear</button>
<button class="btn btn-ghost">View Report</button>
<button class="btn btn-primary">Save Results</button>
</div>
</div>
</div>
</main>
</div>
</div>
</body>
</html>

5
templates/shared.css Normal file
View File

@ -0,0 +1,5 @@
/* shared.css - Override forest theme colors with hex */
[data-theme="forest"] {
--color-primary: #2d6a4f; /* Dark medical green */
--color-primary-content: #ffffff;
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});