clean lets init with simple svelte
This commit is contained in:
parent
bae48fab29
commit
d0350388a0
162
AGENTS.md
162
AGENTS.md
@ -1,162 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,734 +0,0 @@
|
|||||||
# 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
108
README.md
@ -1,108 +0,0 @@
|
|||||||
# 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
|
|
||||||
34261
docs/llms-full_sveltekit.txt
Normal file
34261
docs/llms-full_sveltekit.txt
Normal file
File diff suppressed because it is too large
Load Diff
1852
docs/llms_daisyui.txt
Normal file
1852
docs/llms_daisyui.txt
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
1744
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
|||||||
onlyBuiltDependencies:
|
|
||||||
- esbuild
|
|
||||||
91
src/app.css
91
src/app.css
@ -1,91 +0,0 @@
|
|||||||
@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
15
src/app.d.ts
vendored
@ -1,15 +0,0 @@
|
|||||||
// 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
11
src/app.html
@ -1,11 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,2 +0,0 @@
|
|||||||
export { default as Sidebar } from './layout/Sidebar.svelte';
|
|
||||||
export { default as Header } from './layout/Header.svelte';
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
<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 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
// 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 };
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
export { auth, type User, type AuthState } from './auth';
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,184 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<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()}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# allow crawling everything by default
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [tailwindcss(), sveltekit()]
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user