+
-
+
\ No newline at end of file
diff --git a/src/blog/clqms-roast-Opus.md b/src/blog/clqms-roast-Opus.md
new file mode 100644
index 0000000..b176c01
--- /dev/null
+++ b/src/blog/clqms-roast-Opus.md
@@ -0,0 +1,563 @@
+---
+title: "Database Design Roast: Claude Opus"
+description: "A professional roast of the CLQMS database schema."
+date: 2025-12-12
+order: 6
+tags:
+ - posts
+ - clqms
+layout: clqms-post.njk
+---
+
+# ๐ฅ CLQMS Database Schema: A Professional Roast ๐ฅ
+
+
+> *"I've seen spaghetti code before, but this is spaghetti architecture."*
+
+---
+
+## Executive Summary
+
+After reviewing the CLQMS (Clinical Laboratory Quality Management System) database schema, I regret to inform you that this is not a database design โ it's a **crime scene**. Someone read *"Design Patterns for Dummies"* and decided to implement ALL of them. Simultaneously. Incorrectly.
+
+Let me walk you through this masterpiece of over-engineering.
+
+---
+
+## 1. ๐๏ธ The ValueSet Anti-Pattern: Where Enums Go to Die
+
+### What's Happening
+
+```
+valueset โ VID, SiteID, VSetID, VValue, VDesc, VCategory
+valuesetdef โ VSetID, VSName, VSDesc
+```
+
+**Every. Single. Enum.** In the entire system is crammed into ONE table:
+- Gender
+- Country
+- Religion
+- Ethnicity
+- Marital Status
+- Death Indicator
+- Location Type
+- Test Type
+- Site Type
+- Site Class
+- And probably what you had for lunch
+
+### Why This Is Catastrophic
+
+1. **Zero Type Safety**
+ ```sql
+ -- This is totally valid and will execute without error:
+ UPDATE patient SET Gender =
+ (SELECT VID FROM valueset WHERE VDesc = 'Hospital' AND VSetID = 'LocationType')
+ ```
+ Congratulations, your patient is now a hospital.
+
+2. **The Join Apocalypse**
+ Getting a single patient with readable values requires this:
+ ```php
+ ->join('valueset country', 'country.VID = patient.Country', 'left')
+ ->join('valueset race', 'race.VID = patient.Race', 'left')
+ ->join('valueset religion', 'religion.VID = patient.Religion', 'left')
+ ->join('valueset ethnic', 'ethnic.VID = patient.Ethnic', 'left')
+ ->join('valueset gender', 'gender.VID = patient.Gender', 'left')
+ ->join('valueset deathindicator', 'deathindicator.VID = patient.DeathIndicator', 'left')
+ ->join('valueset maritalstatus', 'maritalstatus.VID = patient.MaritalStatus', 'left')
+ ```
+ That's **7 joins to the same table** for ONE patient query. Your database server is crying.
+
+3. **No Easy Identification**
+ - Primary key is `VID` โ an auto-increment integer
+ - No natural key enforcement
+ - You want "Male"? Good luck remembering if it's VID 47 or VID 174
+
+4. **Maintenance Nightmare**
+ - Adding a new Gender option? Better hope you remember the correct `VSetID`
+ - Want to rename "Other" to "Non-Binary"? Hope you update it in the right row
+ - Database constraints? What are those?
+
+### What Sane People Do
+
+```sql
+-- Option A: Actual ENUMs (MySQL)
+ALTER TABLE patient ADD COLUMN gender ENUM('M', 'F', 'O', 'U');
+
+-- Option B: Dedicated lookup tables with meaningful constraints
+CREATE TABLE gender (
+ code VARCHAR(2) PRIMARY KEY,
+ description VARCHAR(50) NOT NULL
+);
+ALTER TABLE patient ADD FOREIGN KEY (gender_code) REFERENCES gender(code);
+```
+
+---
+
+## 2. ๐ช Organization: The Matryoshka Nightmare
+
+### The Current "Architecture"
+
+```
+Account โ has Parent (self-referencing)
+ โณ has Sites
+ โณ Site โ has Parent (self-referencing)
+ โณ has SiteTypeID โ valueset
+ โณ has SiteClassID โ valueset
+ โณ has Departments
+ โณ Department โ has DisciplineID
+ โณ Discipline โ has Parent (self-referencing)
+
+Plus: Workstations exist somewhere in this chaos
+```
+
+### The Philosophical Question
+
+**What IS an organization in this system?**
+
+| Entity | Has Parent? | Has Site? | Has Account? | Is Self-Referencing? |
+|--------|-------------|-----------|--------------|----------------------|
+| Account | โ
| โ | โ | โ
|
+| Site | โ
| โ | โ
| โ
|
+| Department | โ | โ
| โ | โ |
+| Discipline | โ
| โ
| โ | โ
|
+
+So to understand organizational hierarchy, you need to:
+1. Traverse Account's parent chain
+2. For each Account, get Sites
+3. Traverse each Site's parent chain
+4. For each Site, get Departments AND Disciplines
+5. Traverse each Discipline's parent chain
+6. Oh and don't forget Workstations
+
+You basically need a graph database to query what should be a simple org chart.
+
+### What Normal Systems Do
+
+```sql
+CREATE TABLE organization (
+ id INT PRIMARY KEY,
+ parent_id INT REFERENCES organization(id),
+ type ENUM('company', 'site', 'department', 'discipline'),
+ name VARCHAR(255),
+ code VARCHAR(50)
+);
+```
+
+**One table. One parent reference. Done.**
+
+---
+
+## 3. ๐ Location + LocationAddress: The Pointless Split
+
+### The Crime
+
+```
+location โ LocationID, SiteID, LocCode, Parent, LocFull, LocType
+locationaddress โ LocationID, Street1, Street2, City, Province, PostCode, GeoLocation
+```
+
+**LocationAddress uses LocationID as both Primary Key AND Foreign Key.**
+
+This means:
+- Every location has **exactly one** address (1:1 relationship)
+- You cannot have a location without an address
+- You cannot have multiple addresses per location
+
+### Evidence of the Crime
+
+```php
+public function saveLocation(array $data): array {
+ $db->transBegin();
+ try {
+ if (!empty($data['LocationID'])) {
+ $this->update($LocationID, $data);
+ $modelAddress->update($LocationID, $data); // <-- Always update BOTH
+ } else {
+ $LocationID = $this->insert($data, true);
+ $modelAddress->insert($data); // <-- Always insert BOTH
+ }
+ $db->transCommit();
+ }
+}
+```
+
+You **always** have to save both tables in a transaction. Because they are fundamentally **ONE entity**.
+
+### The Verdict
+
+If data is always created, updated, and deleted together โ **IT BELONGS IN THE SAME TABLE**.
+
+```sql
+-- Just combine them:
+CREATE TABLE location (
+ location_id INT PRIMARY KEY,
+ site_id INT,
+ loc_code VARCHAR(50),
+ loc_full VARCHAR(255),
+ loc_type INT,
+ street1 VARCHAR(255),
+ street2 VARCHAR(255),
+ city INT,
+ province INT,
+ post_code VARCHAR(20),
+ geo_location_system VARCHAR(50),
+ geo_location_data TEXT
+);
+```
+
+You just saved yourself a transaction, a join, and 50% of the headache.
+
+---
+
+## 4. ๐จโโ๏ธ Contact vs Doctor: The Identity Crisis
+
+### The Confusion
+
+```
+contact โ ContactID, NameFirst, NameLast, Specialty, SubSpecialty, Phone...
+contactdetail โ ContactDetID, ContactID, SiteID, OccupationID, JobTitle, Department...
+occupation โ OccupationID, OccupationName...
+```
+
+### The Questions Nobody Can Answer
+
+1. **Is a Contact a Doctor?**
+ - Contact has `Specialty` and `SubSpecialty` fields (doctor-specific)
+ - But also has generic `OccupationID` via ContactDetail
+ - So a Contact is Maybe-A-Doctorโข?
+
+2. **What prevents non-doctors from being assigned as doctors?**
+
+ In `patvisitadt`:
+ ```php
+ 'AttDoc', 'RefDoc', 'AdmDoc', 'CnsDoc' // Attending, Referring, Admitting, Consulting
+ ```
+
+ These store ContactIDs. But there's **zero validation** that these contacts are actually doctors. Your receptionist could be the Attending Physician and the database would happily accept it.
+
+3. **Why does ContactDetail exist?**
+ - It stores the same person's info **per site**
+ - So one person can have different roles at different sites
+ - But `Specialty` is on Contact (not ContactDetail), so a doctor has the same specialty everywhere?
+ - Except `OccupationID` is on ContactDetail, so their occupation changes per site?
+
+### The Solution
+
+```sql
+CREATE TABLE person (
+ person_id INT PRIMARY KEY,
+ first_name VARCHAR(100),
+ last_name VARCHAR(100),
+ -- ... basic personal info
+);
+
+CREATE TABLE doctor (
+ doctor_id INT PRIMARY KEY REFERENCES person(person_id),
+ specialty VARCHAR(100),
+ subspecialty VARCHAR(100),
+ license_number VARCHAR(50)
+);
+
+CREATE TABLE site_staff (
+ person_id INT REFERENCES person(person_id),
+ site_id INT REFERENCES site(site_id),
+ role ENUM('doctor', 'nurse', 'technician', 'admin'),
+ PRIMARY KEY (person_id, site_id)
+);
+```
+
+Now you can actually **enforce** that only doctors are assigned to doctor fields.
+
+---
+
+## 5. ๐ฅ Patient Data: The Table Explosion
+
+### The Current State
+
+| Table | Purpose | Why It Exists |
+|-------|---------|---------------|
+| `patient` | Main patient (30+ columns) | Fair enough |
+| `patidt` | Patient Identifiers | One identifier per patient |
+| `patcom` | Patient Comments | ONE comment per patient (why a whole table?) |
+| `patatt` | Patient "Attachments"... or Addresses? | Stores `Address` as a single string |
+
+### The Crimes
+
+#### Crime 1: Duplicate Address Storage
+
+`patient` table already has:
+```
+Street_1, Street_2, Street_3, City, Province, ZIP
+```
+
+`patatt` table stores:
+```
+Address (as a single string)
+```
+
+**Why do we have both?** Nobody knows. Pick one and commit.
+
+#### Crime 2: One Table for ONE Comment
+
+```php
+class PatComModel {
+ protected $allowedFields = ['InternalPID', 'Comment', 'CreateDate', 'EndDate'];
+
+ public function createPatCom(string $patcom, string $newInternalPID) {
+ $this->insert(["InternalPID" => $newInternalPID, "Comment" => $patcom]);
+ }
+}
+```
+
+A whole table. Foreign key constraints. Model class. CRUD operations. **For one text field.**
+
+Just add `comment TEXT` to the patient table.
+
+#### Crime 3: CSV in a Relational Database
+
+```php
+$patient['LinkTo'] = '1,5,23,47'; // Comma-separated patient IDs
+```
+
+```php
+private function getLinkedPatients(?string $linkTo): ?array {
+ $ids = array_filter(explode(',', $linkTo)); // Oh no
+ return $this->db->table('patient')->whereIn('InternalPID', $ids)->get();
+}
+```
+
+**It's 2025.** Use a junction table:
+
+```sql
+CREATE TABLE patient_link (
+ patient_id INT,
+ linked_patient_id INT,
+ link_type VARCHAR(50),
+ PRIMARY KEY (patient_id, linked_patient_id)
+);
+```
+
+---
+
+## 6. ๐คฎ Patient Admission (ADT): Event Sourcing Gone Wrong
+
+### The Pattern
+
+```
+patvisit โ InternalPVID, PVID, InternalPID (the visit)
+patvisitadt โ PVADTID, InternalPVID, ADTCode, LocationID, AttDoc... (ADT events)
+patdiag โ InternalPVID, DiagCode, Diagnosis
+```
+
+Instead of tracking patient status with simple fields, every Admission/Discharge/Transfer creates a **new row**.
+
+### The Query From Hell
+
+To get the current status of a patient visit:
+
+```php
+->join('(SELECT a1.*
+ FROM patvisitadt a1
+ INNER JOIN (
+ SELECT InternalPVID, MAX(PVADTID) AS MaxID
+ FROM patvisitadt
+ GROUP BY InternalPVID
+ ) a2 ON a1.InternalPVID = a2.InternalPVID AND a1.PVADTID = a2.MaxID
+ ) AS patvisitadt',
+ 'patvisitadt.InternalPVID = patvisit.InternalPVID',
+ 'left')
+```
+
+Every. Single. Query. To get current patient status.
+
+### The Performance Analysis
+
+| Rows in patvisitadt | Query Complexity |
+|---------------------|------------------|
+| 1,000 | Meh, fine |
+| 10,000 | Getting slow |
+| 100,000 | Coffee break |
+| 1,000,000 | Go home |
+
+### What You Should Do
+
+**Option A: Just use status fields**
+```sql
+ALTER TABLE patvisit ADD COLUMN current_status ENUM('admitted', 'discharged', 'transferred');
+ALTER TABLE patvisit ADD COLUMN current_location_id INT;
+ALTER TABLE patvisit ADD COLUMN current_attending_doctor_id INT;
+```
+
+**Option B: If you NEED history, use proper triggers**
+```sql
+CREATE TRIGGER update_current_status
+AFTER INSERT ON patvisitadt
+FOR EACH ROW
+UPDATE patvisit SET current_status = NEW.adt_code WHERE InternalPVID = NEW.InternalPVID;
+```
+
+---
+
+## 7. ๐งช Test Definitions: The Abbreviation Cemetery
+
+### The Tables
+
+| Table | What Is This? | Fields |
+|-------|---------------|--------|
+| `testdefsite` | Test per Site | TestSiteID, TestSiteCode, TestSiteName, SeqScr, SeqRpt, VisibleScr, VisibleRpt... |
+| `testdefgrp` | Test Groups | TestGrpID, TestSiteID, Member |
+| `testdefcal` | Calculated Tests | TestCalID, TestSiteID, DisciplineID, DepartmentID... |
+| `testdeftech` | Technical Details | TestTechID, TestSiteID, DisciplineID, DepartmentID... |
+| `testmap` | ??? | TestMapID, TestSiteID... |
+| `refnum` | Numeric Reference Ranges | RefNumID, TestSiteID, Sex, AgeStart, AgeEnd, Low, High... |
+| `reftxt` | Text Reference Ranges | RefTxtID, TestSiteID, Sex, AgeStart, AgeEnd, RefTxt... |
+| `refvset` | ValueSet Reference | RefVSetID, TestSiteID... |
+| `refthold` | Thresholds | RefTHoldID... |
+
+### The Abbreviation Apocalypse
+
+| Abbreviation | Meaning | Guessability |
+|--------------|---------|--------------|
+| `SeqScr` | Sequence Screen | 2/10 |
+| `SeqRpt` | Sequence Report | 3/10 |
+| `SpcType` | Specimen Type | 4/10 |
+| `VID` | ValueSet ID | 1/10 |
+| `InternalPID` | Internal Patient ID | 5/10 |
+| `InternalPVID` | Internal Patient Visit ID | 4/10 |
+| `PVADTID` | Patient Visit ADT ID | 0/10 |
+| `TestDefCal` | Test Definition Calculation | 3/10 |
+| `RefTHold` | Reference Threshold | 1/10 |
+
+**Pro tip:** If new developers need a glossary to understand your schema, you've failed.
+
+### The Split Between RefNum and RefTxt
+
+For numeric tests: use `refnum`
+For text tests: use `reftxt`
+
+Why not:
+```sql
+CREATE TABLE reference_range (
+ id INT PRIMARY KEY,
+ test_id INT,
+ range_type ENUM('numeric', 'text'),
+ low_value DECIMAL,
+ high_value DECIMAL,
+ text_value VARCHAR(255),
+ -- ... other fields
+);
+```
+
+One table. One query. One life.
+
+---
+
+## 8. ๐ญ Bonus Round: Sins I Couldn't Ignore
+
+### Sin #1: Inconsistent Soft Delete Field Names
+
+| Table | Delete Field | Why Different? |
+|-------|--------------|----------------|
+| Most tables | `EndDate` | ??? |
+| patient | `DelDate` | ??? |
+| patatt | `DelDate` | ??? |
+| patvisitadt | `EndDate` AND `ArchivedDate` AND `DelDate` | ยฏ\\\_(ใ)\_/ยฏ |
+
+### Sin #2: Primary Key With a Trailing Space
+
+In `PatComModel.php`:
+```php
+protected $primaryKey = 'PatComID '; // <-- THERE IS A SPACE HERE
+```
+
+This has either:
+- Never been tested
+- Works by pure accident
+- Will explode randomly one day
+
+### Sin #3: Inconsistent ID Naming
+
+| Column | Location |
+|--------|----------|
+| `InternalPID` | patient, patatt, patcom, patidt, patvisit... |
+| `PatientID` | Also on patient table |
+| `ContactID` | contact |
+| `ContactDetID` | contactdetail |
+| `VID` | valueset |
+| `VSetID` | Also valueset and valuesetdef |
+| `TestSiteID` | testdefsite |
+| `TestGrpID` | testdefgrp |
+
+Pick a convention. ANY convention. Please.
+
+### Sin #4: Multiple Date Tracking Fields with Unclear Purposes
+
+On `testdefsite`:
+- `CreateDate` โ when created
+- `StartDate` โ when... started? Different from created how?
+- `EndDate` โ when ended (soft delete)
+
+### Sin #5: No Data Validation
+
+The `patient` model has 30+ fields including:
+- `Gender` (valueset VID โ could be literally anything)
+- `Religion` (valueset VID โ could be a LocationType)
+- `DeathIndicator` (valueset VID โ could be a Gender)
+
+Zero database-level constraints. Zero model-level validation. Pure vibes.
+
+---
+
+## ๐ The Final Scorecard
+
+| Category | Rating | Notes |
+|----------|--------|-------|
+| **Normalization** | 2/10 | Either over-normalized (LocationAddress) or under-normalized (CSV in columns) |
+| **Consistency** | 1/10 | Every table is a unique snowflake |
+| **Performance** | 3/10 | Those MAX subqueries and 7-way joins will age poorly |
+| **Maintainability** | 1/10 | Good luck onboarding new developers |
+| **Type Safety** | 0/10 | ValueSet is a type-safety black hole |
+| **Naming** | 2/10 | Abbreviation chaos |
+| **Scalability** | 2/10 | Event-sourcing-but-wrong will not scale |
+
+**Overall: 1.5/10** โ *"At least the tables exist"*
+
+---
+
+## ๐ก Recommendations
+
+If you want to fix this (and you should), here's the priority order:
+
+1. **Eliminate the ValueSet monster** โ Replace with proper ENUMs or dedicated lookup tables with foreign key constraints
+
+2. **Combine 1:1 tables** โ Location + LocationAddress, Patient + PatCom (if it's really just one comment)
+
+3. **Fix Patient data model** โ Proper junction tables for PatIdt, PatAtt, and LinkTo
+
+4. **Add current status to PatVisit** โ Denormalize the ADT current state
+
+5. **Standardize naming** โ Pick `*_id`, `*ID`, or `Id` and stick with it. Pick `end_date` or `del_date` for soft deletes.
+
+6. **Add actual constraints** โ Foreign keys that make sense. Check constraints. Not just vibes.
+
+---
+
+## ๐ Conclusion
+
+This schema is what happens when someone:
+- Prioritizes "flexibility" over usability
+- Learns about normalization but not when to stop
+- Discovers self-referencing tables and uses them everywhere
+- Thinks abbreviations save storage space (they don't)
+- Has never had to maintain their own code
+
+The good news: it can be fixed.
+The bad news: it should have been designed correctly the first time.
+
+---
+
+*Document prepared with ๐ฅ and โ*
+
+*May your database queries be fast and your schemas be sane.*
diff --git a/src/blog/clqms-roast-zai.md b/src/blog/clqms-roast-zai.md
new file mode 100644
index 0000000..7d60a21
--- /dev/null
+++ b/src/blog/clqms-roast-zai.md
@@ -0,0 +1,170 @@
+---
+title: "Database Design Roast"
+description: "A legendary roast of the CLQMS database schema, highlighting the architectural hazards and data hell."
+date: 2026-01-07
+order: 7
+tags:
+ - posts
+ - clqms
+layout: clqms-post.njk
+---
+
+# The Database Design from Hell: A Comprehensive Roast
+## *Or: Why your current project makes you want to quit.*
+
+You are right to be sick of this. This document is a masterclass in how **not** to design a database. It takes simple concepts and wraps them in layers of unnecessary, redundant, and contradictory complexity.
+
+Here is the systematic destruction of every data relation and design choice that is causing you pain.
+
+---
+
+## 1. The "Value Set" Disaster (The God Table)
+
+**The Design:**
+Storing every single enumeration (Dropdown list, Flag, Reference Text, Status) in one giant generic table (`ValueSet` / `codedtxtfld`).
+
+**The Roast:**
+* **No Identity:** You mentioned "don't have an easy identifier." This is because they likely didn't use a Surrogate Key (an auto-incrementing ID). They probably used the `Code` (e.g., "M" for Male) as the Primary Key.
+ * *Why it sucks:* If you ever need to change "M" to "Male", you break every single Foreign Key in the database that points to it. You can't update a key that is being used elsewhere. Your data is frozen in time forever.
+* **Performance Killer:** Imagine loading a dropdown for "Gender." The database has to scan a table containing 10,000 rows (all flags, all statuses, all colors, all text results) just to find "M" and "F".
+* **Zero Integrity:** Because it's a generic table, you can't enforce specific rules. You can accidentally delete a value used for "Critical Patient Status" because the table thinks it's just a generic string. There is no referential integrity. It's the Wild West.
+
+---
+
+## 2. The "Organization" Nightmare
+
+**The Design:**
+Attempting to model a global conglomerate structure of Accounts, Sites, Disciplines, Departments, Workstations, and Parent-Child hierarchies.
+
+**The Roast:**
+* **Over-Engineering:** You are building a database for a Laboratory, not the United Nations. Why does a lab system need a recursive `Account` structure that handles "Parent/Child" relationships for companies?
+* **The Blob:** `Account` comes from CRM, but `Site` comes from CRM, yet `Discipline` is internal. You are mixing external business logic (Sales) with internal operational logic (Lab Science).
+* **Identity Confusion:** Is a "Department" a physical place? A group of people? Or a billing category? In this design, it's all three simultaneously. This makes generating a simple report like "Who works in Hematology?" a complex query involving `Organization`, `Personnel`, and `Location`.
+
+---
+
+## 3. The "Location" & "LocationAddress" Split
+
+**The Design:**
+Separating the Location definition (Name, Type) from its Address (Street, City) into two different tables linked 1-to-1.
+
+**The Roast:**
+* **The "Why?" Factor:** Why? Is a Bed (Location) going to have multiple addresses? No. Is a Building going to have multiple addresses? No.
+* **Performance Tax:** Every single time you need to print a label or show where a sample is, you **must** perform a `JOIN`.
+ * *Bad Design:* `SELECT * FROM Location l JOIN LocationAddress a ON l.id = a.id`
+* **The Null Nightmare:** For mobile locations (Home Care), the address is vital. For static locations (Bed 1), the address is meaningless (it's just coordinates relative to the room). By forcing a split, you either have empty rows in `LocationAddress` or you have to invent fake addresses for beds. It's pointless normalization.
+
+---
+
+## 4. The "HostApp" Table
+
+**The Design:**
+A specific table dedicated to defining external applications (`HostApp`) that integrate with the system.
+
+**The Roast:**
+* **The Myth of Modularity:** This table pretends the system is "plug-and-play" with other apps. But look at the Appendices: "Calibration Results SQL Scripts." That's hard-coded SQL, not a dynamic plugin.
+* **Maintenance Hell:** This table implies you need to map every single field from every external app.
+ * *Scenario:* Hospital A uses HIS "MediTech". Hospital B uses HIS "CarePoint".
+ * *Result:* You need a `HostApp` row for MediTech and one for CarePoint. Then you need mapping tables for Patient, Order, Result, etc. You are building an ETL (Extract, Transform, Load) tool inside a Lab database. It's out of scope.
+
+---
+
+## 5. The "Doctor" vs "Contact" Loop
+
+**The Design:**
+A `Contact` table that stores generic people, and a `Doctor` table that... also stores people? Or does it reference Contact?
+
+**The Roast:**
+* **The Infinite Join:** To get a Doctor's name, do you query `Doctor` or `Contact`?
+ * If `Doctor` extends `Contact` (1-to-1), you have to join every time.
+ * If `Doctor` is just a row in `Contact` with a `Type='Doctor'`, why does the `Doctor` table exist?
+* **Semantic Mess:** A "Contact" usually implies "How to reach." A "Doctor" implies "Medical License."
+* **The Failure:** If Dr. Smith retires, do you delete the `Doctor` record? Yes. But then you delete his `Contact` info, so you lose his phone number for historical records. This design doesn't separate the *Role* (Doctor) from the *Entity* (Person). It's a data integrity nightmare.
+
+---
+
+## 6. Patient Data: The Actual Hell
+
+**The Design:**
+Storing Patients, Non-Patients (Blood bags), External QC, and linking/unlinking them all in one messy structure.
+
+**The Roast:**
+* **Blood Bags are not Humans:**
+ * The document explicitly says "mengelola non-patient entity... blood bag."
+ * *Result:* Your `Patient` table has `DateOfBirth` (Required) and `BloodType` (Required). For a Blood Bag, DOB is NULL. For a Human, BloodType might be NULL.
+ * You have created a "Sparse Table" (50% NULLs). It's impossible to index effectively. It breaks the very definition of what a "Patient" is.
+* **The "Link/Unlink" Suicide Pact:**
+ * "Menghubungkan (link)/mengurai(unlink) data pasien."
+ * *Audit Trail Death:* If I link "John Doe" from Site A to "John Doe" from Site B, and then later "Unlink" them, the database loses the history of that decision. Why did we unlink them? Was it a mistake? The design doesn't track the *decision*, it just changes the data.
+* **Confusion:** `Patient Registration` vs `Patient Admission`. Why are these two different giant workflows? In every other system on earth, you Register (create ID) and Admit (start visit). This document treats them like they require NASA-level calculations.
+
+---
+
+## 7. Patient Admission: Revenue Cycle Virulence
+
+**The Design:**
+Admission is tightly coupled with "Pihak yang menanggung biaya" (Payer) and "Tarif" (Price).
+
+**The Roast:**
+* **It's a Billing System, not a Lab System:** This section reads like an Accounting module. The Lab database should not care if the patient pays with BlueCross or Cash.
+* **The Logic Trap:** If the Admission fails because the "Tarif" (Price) is missing, does the Lab stop processing the blood sample?
+ * *According to this design:* Probably yes.
+ * *In reality:* The patient could be dying. Clinical safety should never be blocked by administrative billing data. Mixing these concerns is dangerous.
+
+---
+
+## 8. Test Data: The Maze of Redundancy
+
+**The Design:**
+`testdef`, `testdefsite`, `testdeftech`, `testgrp`, `refnum`, `reftxt`, `fixpanel`.
+
+**The Roast:**
+* **Definition Explosion:**
+ * `testdefsite`: Defines "Glucose" for Site A.
+ * `testdeftech`: Defines "Glucose" for Machine B.
+ * *Reality:* Glucose is Glucose. The chemical reaction doesn't change because the building is different.
+ * *Cost:* You have to update the Reference Range for Glucose in 50 different places if the lab director decides to change it.
+* **The "Group" Soup:**
+ * `Profile` (One tube), `Functional Procedure` (Time series), `Superset` (Billing).
+ * These are stored in `testgrp` as if they are the same thing.
+ * *Failure:** You can't enforce logic. The system allows you to add a "2-Hour Post-Prandial" (Time-based) to a "Lipid Panel" (One-tube) because to the database, they are just "Tests in a Group."
+* **`refnum` vs `reftxt`:**
+ * Why split them? A Reference Range is data. Whether it's "10-20" (Numeric) or "Positive/Negative" (Text) is just a formatting rule. Splitting them into two tables doubles your JOINs and complicates the query logic for no reason.
+
+---
+
+## 9. BONUS: Other Disasters I Found
+
+### A. Equipment & The "Mousepad" Tracking
+**The Roast:**
+The document defines Equipment as: "termasuk UPS, AVR, printer, PC... mouse, keyboard."
+* **The Trap:** Do you really need a depreciation schedule and maintenance log for a mouse? By lumping "IVD Analyzers" (Critical Medical Devices) with "Computer Mice" (Office Supplies), you clutter the Equipment table with garbage data.
+* **Fix:** Separate `CapitalAsset` (Machines) from `InventoryItem` (Supplies).
+
+### B. Specimen: The "Parent" Trap
+**The Roast:**
+Secondary Specimen has a `ParentID` pointing to Primary Specimen.
+* **The Void:** There is no tracking of volume. If Tube A (Parent) has 5ml, and you create Tube B (Child/Aliquot) with 1ml, the database does not know that Tube A now only has 4ml left.
+* **The Consequence:** You cannot do automated inventory. You can't alert the user "Running low on sample!" because the database thinks the sample is infinite.
+
+### C. The "Red Font" Encryption
+**The Roast:**
+"Encrypted = Red Font."
+* **The Joke:** This is technically illiterate. A database stores bytes. It does not store "Red Font."
+* **The Risk:** If they literally store `
PatientName` in the database, you have corrupted your data.
+* **The Reality:** If it's just a UI setting, why is it in the database requirements section? It proves the writer doesn't know the difference between Data Storage and Data Presentation.
+
+---
+
+## Summary: Why you want to vomit
+
+This document is a "Jack of All Trades, Master of None."
+It tries to be:
+1. **CRM** (Contact/Account mgmt)
+2. **ERP** (Inventory/Asset mgmt)
+3. **Billing System** (Admission/Tariff)
+4. **Laboratory Information System** (The actual work)
+
+By trying to do all of this in a single, poorly normalized schema, it creates a **Data Hell** where nothing is reliable, nothing is fast, and changing one thing breaks three others.
+
+**Do not build this "as is."** If you do, you will spend the next 5 years writing `SQL Scripts` to patch the holes in this sinking ship.
\ No newline at end of file
diff --git a/src/blog/clqms-wst-concept.md b/src/blog/clqms-wst-concept.md
index bfddc8a..0d16ab8 100644
--- a/src/blog/clqms-wst-concept.md
+++ b/src/blog/clqms-wst-concept.md
@@ -2,7 +2,7 @@
title: "Project Pandaria: Next-Gen LIS Architecture"
description: "An offline-first, event-driven architecture concept for the CLQMS."
date: 2025-12-19
-order: 6
+order: 8
tags:
- posts
- clqms
diff --git a/src/blog/clqms-wst-database.md b/src/blog/clqms-wst-database.md
index 39294aa..2dbb9ed 100644
--- a/src/blog/clqms-wst-database.md
+++ b/src/blog/clqms-wst-database.md
@@ -2,7 +2,7 @@
title: "Edge Workstation: SQLite Database Schema"
description: "Database design for the offline-first smart workstation."
date: 2025-12-19
-order: 7
+order: 9
tags:
- posts
- clqms
diff --git a/src/blog/index.njk b/src/blog/index.njk
index 035b8fe..67391dd 100644
--- a/src/blog/index.njk
+++ b/src/blog/index.njk
@@ -16,7 +16,7 @@ description: Our projects and technical showcase
-