A Tauri 2.10 (Rust) + Vue 3.5 (TypeScript) + lucide-vue-next desktop application for managing a warfarin anticoagulation clinic. The system bridges HosXP's MySQL database (read-only) with a local SQLite database for clinic-specific tracking, INR trending, dose management, and physician communication slips.
┌─────────────────────────────────────────────────────┐
│ Vue 3.5 Frontend │
│ (TypeScript + Pinia + Vue Router) │
│ lucide-vue-next icon library │
└────────────────────┬────────────────────────────────┘
│ Tauri IPC (invoke / emit)
┌────────────────────▼────────────────────────────────┐
│ Tauri 2.10 Backend (Rust) │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ MySQL Connector │ │ SQLite (local DB) │ │
│ │ (HosXP read-only)│ │ (clinic tracking) │ │
│ └──────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────┘
| Source | Type | Purpose |
|---|---|---|
| HosXP MySQL | Read-only | Patient demographics, drug dispensing, lab results |
| Local SQLite | Read-Write | Clinic enrollment, dose records, appointments, notes |
hn VARCHAR -- hospital number
vstdate DATE -- dispensing date
icode VARCHAR -- drug item code
qty DECIMAL -- quantity dispensed
unitprice DECIMAL -- unit price
icode VARCHAR -- drug code
name VARCHAR -- drug full name
shortname VARCHAR -- drug short name
strength VARCHAR -- drug strength (e.g. "5 mg")
units VARCHAR -- dispensing unit
hn VARCHAR -- hospital number
pname VARCHAR -- title
fname VARCHAR -- first name
lname VARCHAR -- last name
birthday DATE -- date of birth
sex CHAR(1)
addrpart VARCHAR
phone VARCHAR
hn VARCHAR
vn VARCHAR -- visit number
vstdate DATE
doctor VARCHAR
diagtext VARCHAR
lab_order_number VARCHAR
vn VARCHAR
hn VARCHAR
order_date DATE
report_date DATE
department VARCHAR
lab_order_number VARCHAR
lab_items_code VARCHAR
lab_order_result VARCHAR
lab_items_code VARCHAR
lab_items_name VARCHAR
lab_app_order_number VARCHAR
vn VARCHAR
hn VARCHAR
order_date DATE
lab_app_order_number VARCHAR
lab_items_code VARCHAR
lab_order_result VARCHAR
INR lab_items_code =
751Always query BOTHlab_order(vialab_head) ANDlab_app_order(vialab_app_head) and merge results by date, deduplicated, to get the complete INR history for a patient.
| icode | Name (drugitems.name) | Strength (drugitems.strength) |
|---|---|---|
| 1600014 | Warfarin | 5 mg |
| 1600013 | Warfarin | 2 mg |
| 1600024 | Warfarin | 3 mg |
Always query all three icodes together. Display strength from
drugitems.strength, not icode.
CREATE TABLE wf_patients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL UNIQUE,
enrolled_at TEXT NOT NULL,
enrolled_by TEXT,
status TEXT NOT NULL DEFAULT 'active',
-- active | inactive | deceased | transferred | discharged
indication TEXT,
-- AF | DVT | PE | mechanical_valve | other
target_inr_low REAL NOT NULL DEFAULT 2.0,
target_inr_high REAL NOT NULL DEFAULT 3.0,
notes TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);CREATE TABLE wf_visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL,
visit_date TEXT NOT NULL,
inr_value REAL,
inr_source TEXT, -- lab_order | lab_app_order | manual
current_dose_mgday REAL, -- total mg per day
dose_detail TEXT, -- JSON: {mon, tue, wed, thu, fri, sat, sun} in mg
new_dose_mgday REAL, -- prescribed new total mg per day
new_dose_detail TEXT, -- JSON: new daily schedule
new_dose_description TEXT, -- from 0003 migration
selected_dose_option TEXT, -- from 0004 migration
dose_changed INTEGER NOT NULL DEFAULT 0, -- boolean
next_appointment TEXT, -- ISO date
next_inr_due TEXT, -- ISO date
physician TEXT,
notes TEXT,
side_effects TEXT, -- JSON array
adherence TEXT, -- good | fair | poor
created_by TEXT,
reviewed_at TEXT, -- from 0007 migration
reviewed_by TEXT, -- from 0007 migration
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL -- added for sync (CLOUD-SYNC.md)
);CREATE TABLE wf_dose_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL,
changed_at TEXT NOT NULL,
old_dose_mgday REAL,
new_dose_mgday REAL,
old_detail TEXT, -- JSON daily schedule
new_detail TEXT, -- JSON daily schedule
reason TEXT,
inr_at_change REAL,
changed_by TEXT,
created_at TEXT NOT NULL
);CREATE TABLE wf_appointments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL,
appt_date TEXT NOT NULL,
appt_type TEXT, -- inr_check | clinic_visit | urgent
status TEXT NOT NULL DEFAULT 'scheduled',
-- scheduled | completed | missed | cancelled
notes TEXT,
source_visit_id INTEGER, -- from 0005 migration
generated_from_visit INTEGER NOT NULL DEFAULT 0, -- from 0005 migration
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL -- added for sync (CLOUD-SYNC.md)
);CREATE TABLE wf_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL,
event_date TEXT NOT NULL,
event_type TEXT NOT NULL,
-- major_bleeding | minor_bleeding | thromboembolism |
-- hospitalization | death | other
description TEXT,
inr_at_event REAL,
action_taken TEXT,
created_by TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL -- added for sync (CLOUD-SYNC.md)
);CREATE TABLE wf_patient_status_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hn TEXT NOT NULL,
status TEXT NOT NULL,
reason TEXT,
effective_date TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL -- added for sync (CLOUD-SYNC.md)
);CREATE TABLE wf_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);CREATE TABLE wf_drug_interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
icode TEXT NOT NULL,
drug_name TEXT NOT NULL,
strength TEXT,
interaction_type TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(icode)
);Purpose: Query all HosXP patients who have ever received warfarin. Entry point for identifying and enrolling patients into the warfarin clinic.
Behavior:
- Queries
opitemrecejoined withpatientanddrugitemsfor all 3 warfarin icodes - Groups by
hn: shows HN, patient name, age, sex, first dispensing date, last dispensing date, total dispensing visits, strengths received - Filters: date range, enrollment status (all / not enrolled / enrolled)
- Checkbox per row → "นำเข้าคลินิก" button → enrollment modal
- Already-enrolled patients marked with a green badge, cannot be re-enrolled
- Default sort: last dispensing date descending
- Server-side pagination (Tauri command level) — may return thousands of rows
Enrollment Modal fields:
- Indication (AF, DVT, PE, Mechanical Valve, Other)
- Target INR range (low / high — defaults 2.0–3.0, adjustable per indication)
- Enrollment date
- Enrolled by (staff name)
- Notes
Purpose: Overview of all active warfarin clinic patients with at-a-glance INR status and upcoming appointments.
Layout: Sortable table or card grid. Each patient row/card shows:
- HN, name, age, indication badge
- Last INR value + date + colored status indicator:
- 🟢 In range (within target INR)
- 🔴 Above range (INR > target_inr_high)
- 🟡 Below range (INR < target_inr_low)
- ⚪ No recent INR (> 90 days or never)
- Current warfarin dose (mg/day)
- Next appointment date + days until / overdue indicator
- TTR badge (Time in Therapeutic Range, last 6 months) — see Alert Engine
- Quick actions: View Detail, Add Visit, Print Slip
Alert indicators on dashboard:
- 🔴 INR critically high (> 4.0): risk of bleeding
- 🔴 INR critically low (< 1.5): risk of thrombosis
- 🟡 Missed appointment (appt_date passed, status = scheduled)
- 🟡 No INR check in > 90 days
- 🔴 Adverse event recorded in last 30 days
Purpose: Complete clinical view for one warfarin patient.
- Name, HN, age, sex, phone (from HosXP
patient) - Indication badge, target INR range, enrollment date
- Current status badge (active / inactive / deceased / transferred)
- TTR (%) for last 6 months — displayed prominently
- Line chart of all historical INR values over time
- X-axis: date; Y-axis: INR value
- Horizontal band overlay showing target INR range (target_inr_low to target_inr_high)
- Color-coded data points: green = in range, red = high, yellow = low
- Reference lines at 1.5 (critical low) and 4.0 (critical high)
- Data merged from both
lab_order/lab_headandlab_app_order/lab_app_headfor completeness - Tooltips showing exact value, date, and dose at that time
- Zoom/pan support; default view: last 12 months
- Table of all warfarin dispensing records from
opitemrece - Columns: date, drug name, strength, quantity dispensed
- Shows which strength tablets were dispensed per visit
- Chronological list of clinic visits (from SQLite
wf_visits) - Each entry: date, INR, dose at visit, new dose prescribed, next appointment, physician, adherence, notes
- "+ Add Visit" opens a side panel form (see Module 4)
- Current dose input (mg/day or per-day schedule)
- Latest INR input (auto-filled from last lab result)
- Target INR range (from patient record)
- Suggested dose adjustment output based on built-in algorithm:
- If INR in range: maintain dose
- If INR slightly low/high (±0.5 of range): ±10–15% adjustment suggestion
- If INR significantly out of range: ±20–25% adjustment suggestion
- If INR > 4.0: hold recommendation + recheck interval suggestion
- If INR > 5.0: urgent hold + consider reversal note
- The calculation engine is a separate Rust module (bring in existing dose calculator logic)
- Output is a suggestion only — pharmacist/physician confirms before saving
- Upcoming and past appointments from SQLite
wf_appointments - Visual calendar-style mini timeline
- Status color coding: scheduled (blue), completed (green), missed (red), cancelled (gray)
- "+ Schedule Appointment" button
- List of recorded events from
wf_outcomes - "+ Record Event" button → modal with event type, description, INR at event, action taken
- "เปลี่ยนสถานะ" button → modal to set status (inactive, deceased, transferred, discharged) with reason and date
Purpose: Record a warfarin clinic visit. Opens as a right-side panel from /active or /patient/:hn.
Fields:
- Visit date (defaults to today)
- INR value (auto-fetched from HosXP lab — editable if manual entry needed)
- INR source indicator (lab_order / lab_app_order / manual)
- Current dose (mg/day) — auto-filled from last visit record
- Per-day dose schedule (Mon–Sun table, mg per day — supports alternating dose regimens)
- Dose calculator output (read-only suggestion from Module 3e)
- New dose (mg/day) — confirmed by pharmacist, defaults to suggestion
- Per-day new dose schedule (Mon–Sun)
- Next appointment date
- Next INR due date
- Physician name
- Adherence assessment (good / fair / poor)
- Side effects checklist (bleeding gums, bruising, blood in urine/stool, nausea, hair loss, other)
- Notes (free text)
- "บันทึก & พิมพ์ใบ" → save visit + open print slip preview
Purpose: Printable one-page summary for the physician at each clinic visit. Standard A5 or half-A4 format.
Slip Content:
┌─────────────────────────────────────────────┐
│ [HOSPITAL LOGO] Warfarin Care │
├─────────────────────────────────────────────┤
│ ชื่อ-สกุล: [Name] HN: [HN] │
│ อายุ: [Age] เพศ: [Sex] วันที่: [Date] │
│ ข้อบ่งชี้: [Indication] │
│ เป้าหมาย INR: [low] – [high] │
├─────────────────────────────────────────────┤
│ ผล INR วันนี้: [ X.X ] ←── large display │
│ (เป้าหมาย: [low]–[high]) │
│ │
│ INR ย้อนหลัง 3 ครั้ง: │
│ [Date]: X.X [Date]: X.X [Date]: X.X │
├─────────────────────────────────────────────┤
│ ขนาดยาเดิม: [X mg/day] │
│ ตารางการกิน: จ อ พ พฤ ศ ส อา │
│ X X X X X X X mg │
│ │
│ ขนาดยาใหม่ที่แนะนำ: [X mg/day] │
│ ตารางการกิน: จ อ พ พฤ ศ ส อา │
│ X X X X X X X mg │
├─────────────────────────────────────────────┤
│ TTR (6 เดือน): [XX%] │
│ การรับประทานยา: [good/fair/poor] │
│ อาการไม่พึงประสงค์: [...] │
├─────────────────────────────────────────────┤
│ นัดครั้งต่อไป: [Date] │
│ ตรวจ INR ครั้งต่อไป: [Date] │
│ │
│ หมายเหตุ/คำแนะนำ: ________________ │
│ แพทย์ผู้สั่งยา: ________________ │
│ ลายมือชื่อ: ________________ │
└─────────────────────────────────────────────┘
- Printable via Tauri's
printwindow or PDF export - The slip is generated from the most recently saved visit record
- Include a mini INR trend sparkline (last 6 data points) if space allows
Purpose: Clinic-level statistics for quality improvement and accreditation (HA standard).
| Report | Description |
|---|---|
| Patient Census | Active / inactive / discharged count by period |
| TTR Summary | Mean TTR across all active patients (Rosendaal method) |
| INR Distribution | Histogram of all INR values: <1.5, 1.5–2.0, 2.0–3.0, 3.0–4.0, >4.0 |
| Adverse Events Log | All recorded events by type and date |
| Missed Appointments | Patients who missed scheduled visits |
| Dose Adjustment Frequency | How often doses were changed per visit |
| Monthly Cohort | New enrollments per month |
Export: CSV for all reports.
- HosXP MySQL Connection: host, port, database, username, password, test connection button
- Warfarin Drug Codes: view/edit the 3 icode mappings
- Default Target INR Ranges: per indication (AF: 2–3, Mechanical Valve: 2.5–3.5, etc.)
- Staff Names: list for "created by" / "physician" dropdowns
- Hospital Name & Logo: used in printed slips
- Cloud Sync (Supabase): configure backup/restore, push/pull controls, auto-sync settings (see CLOUD-SYNC.md)
- Backup: export SQLite file
Implement the Rosendaal linear interpolation method in Rust:
- Sort all INR values chronologically for a patient
- For each consecutive pair of INR readings (INR₁ on date₁, INR₂ on date₂):
- Linearly interpolate INR for each day between the two readings
- Count each day as "in range" if interpolated INR is within [target_inr_low, target_inr_high]
- TTR = (days in range / total days with data) × 100%
- Calculate for configurable windows: last 3 months, 6 months, 12 months, all-time
- A TTR ≥ 65% is considered acceptable; < 65% triggers a yellow flag; < 50% triggers red
Runs on app startup and every 30 minutes via background Tokio task. Emits events to frontend via Tauri event system.
For each active patient, evaluate:
| Alert | Condition | Severity |
|---|---|---|
| Critical high INR | Latest INR > 4.0 | 🔴 Critical |
| Critical low INR | Latest INR < 1.5 | 🔴 Critical |
| INR above range | Latest INR > target_inr_high | 🟡 Warning |
| INR below range | Latest INR < target_inr_low | 🟡 Warning |
| No recent INR | Last INR date > 90 days ago | 🟡 Warning |
| Missed appointment | appt_date < today AND status = scheduled | 🟡 Warning |
| Low TTR | TTR (6 months) < 50% | 🔴 Critical |
| Adverse event recent | wf_outcomes event_date within 30 days | 🟡 Warning |
#[tauri::command]
async fn search_warfarin_patients(db: State<MySqlPool>, filters: SearchFilters) -> Result<Vec<PatientDrugRecord>>
#[tauri::command]
async fn get_dispensing_history(db: State<MySqlPool>, hn: String) -> Result<Vec<DispensingRecord>>
#[tauri::command]
async fn get_inr_history(db: State<MySqlPool>, hn: String) -> Result<Vec<InrRecord>>
// Queries both lab_order (via lab_head) and lab_app_order (via lab_app_head)
// Merges, deduplicates by date, sorts chronologically
#[tauri::command]
async fn get_latest_inr(db: State<MySqlPool>, hn: String) -> Result<Option<InrRecord>>
#[tauri::command]
async fn test_mysql_connection(config: DbConfig) -> Result<bool>#[tauri::command]
async fn enroll_patient(db: State<SqlitePool>, input: EnrollmentInput) -> Result<i64>
#[tauri::command]
async fn get_active_patients(db: State<SqlitePool>) -> Result<Vec<WfPatientRow>>
#[tauri::command]
async fn get_patient_detail(sqlite: State<SqlitePool>, mysql: State<MySqlPool>, hn: String) -> Result<PatientDetail>
#[tauri::command]
async fn save_visit(db: State<SqlitePool>, visit: VisitInput) -> Result<i64>
#[tauri::command]
async fn get_visit_history(db: State<SqlitePool>, hn: String) -> Result<Vec<WfVisit>>
#[tauri::command]
async fn schedule_appointment(db: State<SqlitePool>, appt: AppointmentInput) -> Result<i64>
#[tauri::command]
async fn record_adverse_event(db: State<SqlitePool>, event: OutcomeInput) -> Result<i64>
#[tauri::command]
async fn calculate_ttr(sqlite: State<SqlitePool>, mysql: State<MySqlPool>, hn: String, window_days: u32) -> Result<f64>
#[tauri::command]
async fn suggest_dose(current_dose: f64, current_inr: f64, target_low: f64, target_high: f64) -> Result<DoseSuggestion>
#[tauri::command]
async fn get_patient_alerts(sqlite: State<SqlitePool>, mysql: State<MySqlPool>) -> Result<Vec<PatientAlert>>
#[tauri::command]
async fn update_patient_status(db: State<SqlitePool>, hn: String, status: String, reason: String) -> Result<()>#[tauri::command]
async fn save_supabase_config(app: tauri::AppHandle, url: String, anon_key: String) -> Result<(), String>
#[tauri::command]
async fn test_supabase_connection(url: String, anon_key: String) -> Result<bool, String>
#[tauri::command]
async fn push_to_supabase(app: tauri::AppHandle, sqlite: State<SqlitePool>) -> Result<SyncResult, String>
#[tauri::command]
async fn pull_from_supabase(app: tauri::AppHandle, sqlite: State<SqlitePool>) -> Result<SyncResult, String>
#[tauri::command]
async fn get_sync_status(app: tauri::AppHandle, sqlite: State<SqlitePool>) -> Result<SyncStatus, String>
#[tauri::command]
async fn get_sync_summary(app: tauri::AppHandle) -> Result<SyncSummary, String>
// Returns { has_anon_key: bool, supabase_url: String | null }pub struct DoseSuggestion {
pub suggested_dose_mgday: f64,
pub adjustment_percent: f64,
pub recommendation: String, // human-readable Thai text
pub urgency: String, // normal | caution | urgent | hold
pub recheck_days: u32, // suggested days until next INR check
}
pub fn suggest_dose(current_dose: f64, inr: f64, target_low: f64, target_high: f64) -> DoseSuggestion {
// INR in range: maintain, recheck in 28–42 days
// INR 0.1–0.5 below low: increase 10%, recheck 14 days
// INR > 0.5 below low: increase 15–20%, recheck 7–14 days
// INR 0.1–0.5 above high: decrease 10%, recheck 14 days
// INR 0.5–1.0 above high: decrease 15–20%, recheck 7 days
// INR 4.0–5.0: hold 1 dose, decrease 20–25%, recheck 3–7 days
// INR > 5.0: hold, urgent review, recheck 1–3 days
// Round to nearest 0.5 mg/day practical dose
}Integrate existing dose calculator logic from previous project here. This module is isolated and unit-testable.
/ → redirect to /screening
/screening → Module 1: Drug screening from HosXP
/active → Module 2: Active patients dashboard
/patient/:hn → Module 3: Patient detail
/slip/:visit_id → Module 5: Physician slip (print view)
/reports → Module 6: Reports
/settings → Module 7: Settings
| Store | Responsibility |
|---|---|
usePatientStore |
Active patient list, enrollment, status updates |
useScreeningStore |
HosXP search results, pending enrollment queue |
useVisitStore |
Current visit form state, dose suggestion |
useAlertStore |
Computed alerts, badge counts |
useSettingsStore |
DB config, drug codes, default INR ranges, staff list |
useSyncStore |
Cloud sync status, push/pull to Supabase (CLOUD-SYNC.md) |
DESIGN.mdis the single source of truth for all visual design decisions. ReadDESIGN.mdin full before writing any UI code. Use token names fromDESIGN.mddirectly — never hardcode hex values, pixel sizes, or shadow strings.
Key pointers into DESIGN.md:
- Colors — use the Brand & Accent, Surface, Text, and Semantic token sets. Primary CTA uses
{colors.primary}(black);{colors.brand-yellow}is reserved for the wordmark and promo elements only. For INR status mapping:- In range →
{colors.success-accent}(green) - Above/below range →
{colors.brand-coral}or{colors.brand-orange-light}(warning) - Critical (INR > 4.0 or < 1.5) →
{colors.brand-red}/{colors.brand-red-dark} - No recent data →
{colors.stone}(muted)
- In range →
- TTR badge — pill shape (
{rounded.full}), colored by threshold: ≥65% use{colors.success-accent}, 50–64% use{colors.brand-coral}, <50% use{colors.brand-red-dark}; foreground always{colors.on-primary}(white) - Typography — use Roobert PRO across all surfaces per the type hierarchy table in DESIGN.md (hero-display → body-sm → micro). Apply negative letter-spacing at heading sizes exactly as specified. Button labels use
{typography.button-md}. - Spacing — 4px base unit; use named spacing tokens (
{spacing.xs}through{spacing.section-lg}). Card internal padding:{spacing.xl}compact,{spacing.xxl}feature panels. - Border Radius —
{rounded.md}(8px) for inputs;{rounded.xl}(16px) for standard cards;{rounded.xxxl}(28px) for pastel feature cards;{rounded.full}(9999px) for all buttons, pill tabs, and status badges. Never soften button corners. - Borders & Shadows —
{colors.hairline}for standard 1px borders;{colors.hairline-soft}for table row dividers. Elevation levels 0–4 from the Elevation & Depth section; use Level 2 for content cards, Level 4 for modals. - Buttons —
button-primary(black pill) for all primary actions;button-secondary(outlined pill) for secondary;button-ghostfor quiet actions. All buttons use{rounded.full}. - Cards —
card-basefor standard content;card-feature-coral/card-feature-teal/card-feature-yellowfor pastel accent panels (e.g. alert summary cards, stat panels). - Badges —
badge-tag-coralfor warning indicators;badge-successfor in-range/completed;badge-tag-purplefor informational. All use{rounded.full}and{typography.caption-bold}. - Comparison table — use
comparison-table+comparison-rowcomponent specs for the dispensing history and report tables. - Icons —
lucide-vue-nextexclusively. Size and visual weight should align with surrounding typography scale from DESIGN.md. - Print slip —
@media print:{colors.canvas}background,{colors.ink}text,{colors.hairline}table borders, elevation Level 0 (no shadows). Typography follows DESIGN.md scale; no Roobert PRO fallback issues in print since it is a desktop app. - Accessibility — focus states, contrast ratios, touch target sizes, and interactive state behaviors are defined in DESIGN.md Accessibility section. Follow them for all interactive elements.
| Layer | Technology |
|---|---|
| Desktop shell | Tauri 2.10 |
| Backend language | Rust (stable) |
| MySQL driver | sqlx with MySQL feature |
| SQLite driver | sqlx with SQLite feature |
| Frontend framework | Vue 3.5 (Composition API, <script setup>) |
| Language | TypeScript 5 |
| State management | Pinia |
| Routing | Vue Router 4 |
| Icons | lucide-vue-next |
| Chart library | unovis or lightweight-charts (INR trend chart) |
| Build tool | Vite |
| Styling | See DESIGN.md |
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
tauri-plugin-store = "2"
tauri-plugin-dialog = "2"
aes-gcm = "0.10"
rand = "0.8"
base64 = "0.22"
sqlx = { version = "0.8", features = ["mysql", "sqlite", "runtime-tokio", "chrono", "macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
thiserror = "2"
regex = "1"
# For Cloud Sync (CLOUD-SYNC.md)
reqwest = { version = "0.12", features = ["json"] }
uuid = { version = "1", features = ["v4", "serde"] }This project is a Cargo workspace (see
AGENTS-RUST.md§14 Project Overrides). Pure domain logic lives incrates/warfarin-core, the sqlx data layer incrates/warfarin-db, andsrc-tauri/is a thin Tauri wrapper.crates/*MUST NOT depend ontauri.
warfarin-care/
├── Cargo.toml # [workspace] root — no [workspace.dependencies]
├── .cargo/config.toml # ws-check / ws-test / ws-clippy aliases
├── .clippy.toml # msrv = "1.85"
├── rustfmt.toml # shared formatting (max_width=100, tab_spaces=2)
├── crates/
│ ├── warfarin-core/ # pure domain logic — NO sqlx, NO tauri
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── encrypt.rs # AES-256-GCM encryption for credentials
│ │ ├── screening.rs # normalize_search_filters (input clamping)
│ │ ├── pills.rs # pill decomposition + dispense summary
│ │ ├── dose/
│ │ │ ├── mod.rs
│ │ │ ├── calculator.rs # suggest_dose, suggest_dose_from_daily, TTR
│ │ │ └── usage_parser.rs # parse dispensing usage text → DoseSchedule
│ │ └── models/
│ │ ├── mod.rs
│ │ ├── patient.rs
│ │ ├── inr.rs
│ │ ├── visit.rs # DoseSchedule, WfVisit, DoseSuggestion, ...
│ │ ├── appointment.rs
│ │ ├── alert.rs
│ │ ├── dispensing.rs
│ │ ├── outcome.rs # Adverse events model
│ │ └── interaction.rs # Drug interaction model
│ └── warfarin-db/ # sqlx data layer — NO tauri
│ ├── Cargo.toml
│ ├── migrations/ # 0001..0010 SQL migrations (sqlx::migrate!)
│ └── src/
│ ├── lib.rs
│ ├── mysql.rs # HosXP read-only queries: screening, dispensing, INR
│ ├── sqlite.rs # local CRUD, AppState, map_visit_row
│ └── sync_models.rs # cloud-sync row types (derive sqlx::FromRow)
├── src-tauri/ # thin Tauri wrapper — commands + IPC glue only
│ ├── Cargo.toml # depends on warfarin-core + warfarin-db (path)
│ └── src/
│ ├── main.rs
│ ├── lib.rs # plugin setup, AppState wiring, 46 commands
│ └── commands/
│ ├── mod.rs
│ ├── screening.rs # search_warfarin_patients
│ ├── patients.rs # enroll, get_active, update_status
│ ├── visits.rs # save_visit, get_visit_history, suggest_dose
│ ├── inr.rs # get_inr_history, get_latest_inr
│ ├── appointments.rs # schedule, update status
│ ├── alerts.rs # get_patient_alerts
│ ├── reports.rs # TTR, census, adverse events
│ ├── settings.rs # test_connection, config CRUD, keyring
│ ├── outcomes.rs # record_adverse_event
│ ├── interaction.rs # drug interactions CRUD (0006)
│ ├── sync.rs # Supabase push/pull (CLOUD-SYNC.md)
│ └── slip.rs # PDF export (path validation is FS glue)
├── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router/index.ts
│ ├── stores/
│ │ ├── patient.ts
│ │ ├── screening.ts
│ │ ├── visit.ts
│ │ ├── alerts.ts
│ │ ├── settings.ts
│ │ └── sync.ts # Cloud sync store (CLOUD-SYNC.md)
│ ├── views/
│ │ ├── ScreeningView.vue
│ │ ├── ActiveView.vue
│ │ ├── PatientDetailView.vue
│ │ ├── SlipView.vue
│ │ ├── ReportsView.vue
│ │ └── SettingsView.vue
│ ├── components/
│ │ ├── layout/
│ │ │ ├── AppSidebar.vue
│ │ │ └── AppHeader.vue
│ │ ├── screening/
│ │ │ ├── PatientTable.vue
│ │ │ └── EnrollModal.vue
│ │ ├── active/
│ │ │ ├── PatientRow.vue
│ │ │ ├── InrStatusBadge.vue
│ │ │ ├── TtrBadge.vue
│ │ │ └── AlertBadge.vue
│ │ ├── patient/
│ │ │ ├── InrTrendChart.vue # line chart with target band
│ │ │ ├── DispensingTable.vue
│ │ │ ├── VisitList.vue
│ │ │ ├── DoseCalculatorPanel.vue
│ │ │ ├── AppointmentTimeline.vue
│ │ │ ├── AdverseEventList.vue
│ │ │ └── StatusChangeModal.vue
│ │ ├── visit/
│ │ │ ├── VisitFormPanel.vue # side panel visit entry
│ │ │ └── DayDoseTable.vue # Mon–Sun per-day dose grid
│ │ ├── slip/
│ │ │ └── PhysicianSlip.vue # printable slip component
│ │ ├── settings/
│ │ │ └── SyncPanel.vue # Cloud sync config (CLOUD-SYNC.md)
│ │ └── shared/
│ │ ├── StatusBadge.vue
│ │ ├── ConfirmDialog.vue
│ │ └── PillBadge.vue
│ └── types/
│ ├── patient.ts
│ ├── inr.ts
│ ├── visit.ts
│ ├── appointment.ts
│ ├── alert.ts
│ └── dispensing.ts
├── DESIGN.md
├── AGENTS.md
├── AGENTS-RUST.md # Rust workspace rules + project overrides
└── CLOUD-SYNC.md # Cloud sync implementation spec
- Three warfarin icodes: Always query all three (
1600014,1600013,1600024) together. Display strength fromdrugitems.strength. - Dual INR lab source: Always merge
lab_order(vialab_head) andlab_app_order(vialab_app_head) for INR code751. Deduplicate by date — preferlab_orderif both exist on the same date. - HosXP is read-only: Never write to HosXP MySQL. All clinic data lives in SQLite only.
- Alternating dose regimens: Warfarin is often prescribed on alternating days (e.g. 5 mg Mon/Wed/Fri, 2.5 mg other days). The dose schedule must support per-day (Mon–Sun) entry.
- TTR ≥ 65% is the international quality benchmark (AHA/ACC guideline). Flag patients below this threshold.
- Target INR range by indication:
- AF, DVT, PE: 2.0–3.0
- Mechanical mitral valve: 2.5–3.5
- Mechanical aortic valve (bileaflet): 2.0–3.0
- Recurrent VTE on warfarin: 2.5–3.5
- These defaults are configurable in Settings
- Buddhist Era display: Show all dates in Thai format (วัน/เดือน/พ.ศ.) in the UI. Store as ISO 8601 (CE) in SQLite and MySQL.
- Print slip must be functional offline — all data rendered from SQLite + most recent MySQL INR fetch cached in the visit record.
- INR > 5.0: Always surface as a critical alert regardless of target range; this is a medical emergency threshold.
- Recheck interval guidance: After dose change, next INR due date defaults to 7–14 days (configurable per dose change magnitude). After stable INR in range, interval extends to 28–42 days.
- Use
sqlx::migrate!()with embedded SQLite migrations for automatic schema setup on first run - Cache HosXP INR results in the
wf_visitsrecord (inr_sourcefield) to support offline slip printing - The alert engine runs as a background Tokio task; communicate to frontend via
tauri::Emitterevents - INR trend chart: use a charting library that supports SVG export (for potential slip embedding); keep bundle size minimal
- All dose arithmetic uses
f64with rounding to nearest 0.5 mg/day practical step - The
dose/calculator.rsmodule is pure functions with no I/O — fully unit-testable with#[cfg(test)] - Server-side pagination in
search_warfarin_patientsTauri command (limit/offset parameters) - Store MySQL credentials using Tauri's OS keychain plugin (
tauri-plugin-strongholdortauri-plugin-os) - Design tokens: The canonical INR/TTR color table is
DESIGN.md§"Semantic — INR Status (Clinical Convention)". A convenience lookup table in §"INR Status Colors (for all components)" mirrors it — keep them in sync. - Encryption key storage: The 32-byte AES key is held in the OS keychain (service
warfarin-care, usermysql-encryption-key) via thekeyringcrate. A one-time migration lifts any legacy key stored inwf_settings(encryption_keyrow) into the keychain on first run, then deletes the row.