# AGENTS.md - Coding Guidelines for CLQMS Frontend

SvelteKit frontend for Clinical Laboratory Quality Management System. Uses Svelte 5 runes, TailwindCSS 4, DaisyUI, and Lucide icons.

## Commands

```bash
# Development
pnpm run dev        # Start development server
pnpm run build      # Production build (outputs to build/)
pnpm run preview    # Preview production build
pnpm run prepare    # Sync SvelteKit (runs on install)

# Package management
pnpm install        # Install dependencies (use pnpm, not npm/yarn)

# Testing (not configured yet - add when needed)
# vitest run src/path/to/test.js  # Run single test
# vitest                          # Run tests in watch mode
# npx playwright test             # E2E tests
```

**No ESLint/Prettier configured yet.** When adding: configure in `vite.config.js` or separate config files.

## Code Style

- **ES Modules**: `import`/`export` (type: "module")
- **Semicolons**: Required
- **Quotes**: Single quotes
- **Indentation**: 2 spaces
- **Trailing commas**: In multi-line objects/arrays
- **JSDoc**: Document all exported functions with `@param` and `@returns`

### Imports Order

1. Svelte (`svelte`, `$app/*`)
2. `$lib/*` (stores, api, components, utils)
3. External libraries (`lucide-svelte`)
4. Relative imports (minimize, prefer `$lib`)

## Naming Conventions

- **Components**: PascalCase (`LoginForm.svelte`, `PatientFormModal.svelte`)
- **Files/Routes**: lowercase with hyphens (`+page.svelte`, `user-profile/`)
- **Variables**: camelCase (`isLoading`, `userName`)
- **Constants**: UPPER_SNAKE_CASE (`API_URL`, `STORAGE_KEY`)
- **Stores**: camelCase, descriptive (`auth`, `config`, `userStore`)
- **Event handlers**: prefix with `handle` (`handleSubmit`, `handleClick`)
- **Form state**: `formLoading`, `formError`, `deleteConfirmOpen`

## Svelte 5 Component Structure

```svelte
<script>
  // 1. Imports
  import { onMount } from 'svelte';
  import { goto } from '$app/navigation';
  import { auth } from '$lib/stores/auth.js';
  import { User } from 'lucide-svelte';

  // 2. Props with $bindable
  let { open = $bindable(false), title = '', children, footer } = $props();

  // 3. State
  let loading = $state(false);
  let formData = $state({ name: '' });

  // 4. Derived
  let isValid = $derived(formData.name.length > 0);

  // 5. Effects
  $effect(() => { /* side effects */ });

  // 6. Handlers
  function handleSubmit() { /* impl */ }
</script>
```

## TypeScript Types

```typescript
// src/lib/types/ - Type definitions
export type TestType = 'TEST' | 'PARAM' | 'CALC' | 'GROUP' | 'TITLE';

export interface TestSummary {
  TestSiteID: number;
  TestSiteCode: string;
  TestSiteName: string;
  TestType: TestType;
}

// Use JSDoc for type imports in JS files
/** @typedef {import('$lib/types/test.types.js').TestSummary} TestSummary */
```

## API Patterns

```javascript
// src/lib/api/client.js - Use these helpers
import { get, post, put, patch, del } from '$lib/api/client.js';

// Feature endpoints (with JSDoc)
/** @param {Object} params - Query parameters */
export async function fetchItems(params = {}) {
  const query = new URLSearchParams(params).toString();
  return get(query ? `/api/items?${query}` : '/api/items');
}

export async function createItem(data) {
  return post('/api/items', data);
}

export async function updateItem(id, data) {
  return patch(`/api/items/${id}`, data);
}

export async function deleteItem(id) {
  return del(`/api/items/${id}`);
}
```

## Store Patterns

```javascript
import { writable } from 'svelte/store';
import { browser } from '$app/environment';

function createStore() {
  const getInitialState = () => {
    if (!browser) return { data: null };
    return JSON.parse(localStorage.getItem('key'));
  };

  const { subscribe, set, update } = writable(getInitialState());

  return {
    subscribe,
    setData: (data) => {
      if (browser) localStorage.setItem('key', JSON.stringify(data));
      set({ data });
    }
  };
}
```

## Component Patterns

### Modal with Snippets

```svelte
<Modal bind:open={showModal} title="Edit Item" size="lg">
  {#snippet children()}
    <form onsubmit={handleSubmit}>
      <input class="input input-bordered" bind:value={formData.name} />
    </form>
  {/snippet}
  {#snippet footer()}
    <button class="btn btn-ghost" onclick={() => showModal = false}>Cancel</button>
    <button class="btn btn-primary" onclick={handleSubmit}>Save</button>
  {/snippet}
</Modal>
```

### Form with Validation

```javascript
let formLoading = $state(false);
let formError = $state('');
let formData = $state({ username: '', password: '' });

function validateForm() {
  formError = '';
  if (!formData.username.trim()) {
    formError = 'Username is required';
    return false;
  }
  return true;
}

async function handleSubmit() {
  if (!validateForm()) return;
  formLoading = true;
  try {
    await api.submit(formData);
    toastSuccess('Success');
  } catch (err) {
    formError = err.message;
  } finally {
    formLoading = false;
  }
}
```

## Error Handling

```javascript
try {
  const result = await api.fetchData();
  toastSuccess('Operation successful');
} catch (err) {
  const message = err.message || 'An unexpected error occurred';
  toastError(message);
  console.error('Operation failed:', err);
}
```

## Styling (DaisyUI + Tailwind)

- **Components**: `btn`, `card`, `alert`, `input`, `navbar`, `modal`, `dropdown`, `menu`
- **Colors**: `primary` (emerald), `secondary` (dark blue), `accent` (royal blue)
- **Sizes**: `btn-sm`, `input-sm`, `select-sm` for compact forms
- **Custom**: `.compact-y`, `.compact-p`, `.compact-input`, `.compact-btn`, `.compact-card`

### Input with Icons (DaisyUI Pattern)

**Correct way** - Use DaisyUI's built-in input with icon support:

```svelte
<!-- WRONG: Absolute positioning (icons may not render) -->
<div class="relative">
  <span class="absolute left-3 top-1/2 -translate-y-1/2">
    <User class="w-4 h-4" />
  </span>
  <input class="input input-sm input-bordered pl-9" />
</div>

<!-- CORRECT: DaisyUI input with flex layout -->
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
  <User class="w-4 h-4 text-gray-400" />
  <input
    type="text"
    class="grow bg-transparent outline-none"
    placeholder="Enter value..."
    bind:value={someValue}
  />
</label>
```

**Key points:**
- Use `<label>` element as the input container with `input input-bordered` classes
- Add `flex items-center gap-2` for layout
- Icon is a **sibling** of the input, not absolutely positioned
- Input uses `class="grow bg-transparent outline-none"` to fill space
- Use `focus-within:input-primary` for focus state on the container
- Always add `text-gray-400` or similar color to the icon for visibility

## Project Structure

```
src/
  lib/
    api/          # API clients per feature
    stores/       # Svelte stores (auth, config, valuesets)
    components/   # Reusable components (Modal, DataTable, Sidebar)
    utils/        # Utilities (toast, helpers)
    types/        # TypeScript type definitions
  routes/         # SvelteKit routes
    (app)/        # Route groups (protected)
      dashboard/
      patients/
      master-data/
    login/        # Public routes
```

## Authentication

- Check `$auth.isAuthenticated` in layout `onMount`
- Redirect to `/login` if unauthenticated using `goto('/login')`
- API client auto-redirects on 401

## LocalStorage

- Only access in browser: check `browser` from `$app/environment`
- Use descriptive keys: `clqms_username`, `clqms_remember`, `auth_token`

## Important Notes

- Uses Svelte 5: `$props`, `$state`, `$derived`, `$effect`, `$bindable`
- Static adapter configured for static export
- Runtime config allows API URL changes without rebuild
- API proxy: `/api` → `http://localhost:8000` (dev)
- API Docs: https://clqms01-api.services-summit.my.id/swagger/
