diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d4a4526
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# DocLink Web
+
+Server-rendered rebuild of the DocLink web app, following the structure in `project-specs/`.
+
+## Stack
+
+- Node.js HTTP server
+- HTMX for partial updates
+- Custom responsive CSS
+- Upstream API adapter for auth, order, result, FPP, password change, and special message flows
+
+## Run
+
+```bash
+npm start
+```
+
+Open:
+
+```text
+http://localhost:5173
+```
+
+## Notes
+
+- Login calls the upstream API from `project-specs/API_ENDPOINTS.md`.
+- If the upstream login rejects the credentials or is unavailable, demo mode creates a local session so the UI can still be exercised.
+- Order search, order detail helpers, FPP loading, password change, and special message save are wired through the API adapter with mock fallback for local preview.
+
+## Main Files
+
+- `server.js`
+- `styles.css`
+- `package.json`
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9ee4446
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "doclink-web",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..83a446e
--- /dev/null
+++ b/server.js
@@ -0,0 +1,1903 @@
+import http from "node:http";
+import { readFile } from "node:fs/promises";
+import { extname } from "node:path";
+
+const PORT = Number(process.env.PORT || 5173);
+const API_BASE =
+ process.env.DOCLINK_API_BASE ||
+ "https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra";
+
+const sessionKey = "doclink_session";
+
+const mockUser = {
+ name: "dr. Fajri",
+ role: "Dokter Mitra",
+ hospital: "Pramita Bandungraya",
+ doctorId: "DR-10024",
+};
+
+const mockOrders = [
+ {
+ id: "ORD-24001",
+ patient: "Siti Amelia",
+ doctor: "dr. Fajri",
+ updated: "2m ago",
+ status: "Processing",
+ tone: "warning",
+ mode: "Inpatient",
+ age: "34",
+ gender: "F",
+ tests: ["Hematology", "Glucose", "Urine"],
+ diagnosis: "Check-up rutin",
+ message: "Prioritize fasting sample and note dizziness symptom.",
+ },
+ {
+ id: "ORD-24002",
+ patient: "Budi Santoso",
+ doctor: "dr. Fajri",
+ updated: "14m ago",
+ status: "Ready",
+ tone: "success",
+ mode: "Outpatient",
+ age: "52",
+ gender: "M",
+ tests: ["Lipid Profile", "Liver Function"],
+ diagnosis: "Kontrol hipertensi",
+ message: "Call if LDL exceeds threshold.",
+ },
+ {
+ id: "ORD-24003",
+ patient: "Nia Putri",
+ doctor: "dr. Fajri",
+ updated: "40m ago",
+ status: "Needs review",
+ tone: "danger",
+ mode: "Emergency",
+ age: "27",
+ gender: "F",
+ tests: ["Electrolytes", "CBC", "CRP"],
+ diagnosis: "Dehydration",
+ message: "Urgent release once CRP is complete.",
+ },
+];
+
+const mockResults = [
+ {
+ id: "RES-8821",
+ patient: "Siti Amelia",
+ test: "CBC",
+ status: "Released",
+ tone: "success",
+ date: "13 Apr 2026",
+ summary: "Hemoglobin slightly below baseline.",
+ value: "11.2 g/dL",
+ },
+ {
+ id: "RES-8822",
+ patient: "Budi Santoso",
+ test: "Lipid Profile",
+ status: "Pending",
+ tone: "warning",
+ date: "13 Apr 2026",
+ summary: "Awaiting final approval.",
+ value: "LDL 145 mg/dL",
+ },
+ {
+ id: "RES-8823",
+ patient: "Nia Putri",
+ test: "CRP",
+ status: "Review",
+ tone: "danger",
+ date: "12 Apr 2026",
+ summary: "Result flagged for clinical review.",
+ value: "18.4 mg/L",
+ },
+];
+
+const mockFppGroups = [
+ { group: "HEMATOLOGI", count: 6, desc: "Complete blood count and related panels." },
+ { group: "KLINIK RUTIN", count: 5, desc: "Daily screening and basic chemistry." },
+ { group: "IMUNOLOGI", count: 3, desc: "Inflammation and antibody markers." },
+ { group: "URINALISA", count: 4, desc: "Urine screening and microscopic checks." },
+];
+
+const mockPatients = [
+ { name: "Siti Amelia", mrn: "MRN-10011", gender: "Female", lastVisit: "Today", note: "Active order" },
+ { name: "Budi Santoso", mrn: "MRN-10012", gender: "Male", lastVisit: "Yesterday", note: "Repeat visit" },
+ { name: "Nia Putri", mrn: "MRN-10013", gender: "Female", lastVisit: "12 Apr", note: "New registration" },
+];
+
+function escapeHtml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+function statusClass(status) {
+ const mapping = {
+ Processing: "warning",
+ Ready: "success",
+ "Needs review": "danger",
+ Released: "success",
+ Pending: "warning",
+ Review: "danger",
+ };
+ return mapping[status] || "neutral";
+}
+
+function statusBadge(text) {
+ return `${escapeHtml(text)} `;
+}
+
+function icon(name) {
+ const map = {
+ search:
+ ' ',
+ plus:
+ ' ',
+ bell:
+ ' ',
+ arrow:
+ ' ',
+ login:
+ ' ',
+ };
+ return map[name] || "";
+}
+
+function layout(title, body, { authenticated = false, activePath = "/", subtitle = "", shell = true } = {}) {
+ const nav = authenticated ? desktopNav(activePath) : "";
+ const mobile = authenticated ? mobileNav(activePath) : "";
+ const header = authenticated ? topbar(activePath, subtitle) : "";
+ return `
+
+
+
+
+ ${escapeHtml(title)}
+
+
+
+
+
+
+
+
+
+ ${shell && authenticated ? `
+
+ ${nav}
+
+
+ ${header}
+ ${body}
+
+
+ ${mobile}
+
+ ` : body}
+
+
+`;
+}
+
+function topbar(activePath, subtitle) {
+ const titleMap = {
+ "/": ["Dashboard", "Overview of orders, results, and work queues."],
+ "/orders": ["Orders", "Search, review, and create patient orders."],
+ "/results": ["Results", "Monitor released and pending laboratory results."],
+ "/fpp": ["FPP", "Browse grouped reference packages and filters."],
+ "/patients": ["Patients", "Landing for patient registration and lookup."],
+ "/settings": ["Settings", "Account profile and password management."],
+ };
+ const [title, fallbackSubtitle] = titleMap[activePath] || ["DocLink Web", "Responsive clinical workflow shell."];
+ const searchForm = activePath === "/orders"
+ ? `
+
+ `
+ : activePath === "/results"
+ ? `
+
+ `
+ : `
+ Search orders
+ `;
+ return `
+
+ `;
+}
+
+function desktopNav(activePath) {
+ const items = [
+ ["/", "Home", "Dashboard"],
+ ["/orders", "Order", "Create & list"],
+ ["/results", "Result", "History"],
+ ["/fpp", "FPP", "Catalog"],
+ ["/settings", "Akun", "Profile"],
+ ];
+ const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
+ return `
+
+ `;
+}
+
+function mobileNav(activePath) {
+ const items = [
+ ["/", "Home", "Dashboard"],
+ ["/orders", "Order", "Create"],
+ ["/results", "Result", "History"],
+ ["/fpp", "FPP", "Catalog"],
+ ["/settings", "Akun", "Profile"],
+ ];
+ const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
+ return `
+
+
+
+ `;
+}
+
+function panelHeader(title, text, action = "") {
+ return `
+
+ `;
+}
+
+function dashboardPage() {
+ const stats = [
+ { label: "Orders today", value: "28", trend: "+12%", hint: "Vs yesterday" },
+ { label: "Results pending", value: "7", trend: "-3%", hint: "Still waiting release" },
+ { label: "FPP items", value: "14", trend: "+2", hint: "Active reference set" },
+ { label: "Special messages", value: "5", trend: "+1", hint: "Need follow-up" },
+ ];
+ const shortcuts = [
+ { title: "New order", desc: "Start registration flow", href: "/orders/new" },
+ { title: "Pending results", desc: "Review unreleased items", href: "/results/pending" },
+ { title: "FPP catalog", desc: "Inspect lab groups", href: "/fpp" },
+ { title: "Change password", desc: "Update account security", href: "/settings/change-password" },
+ ];
+ return `
+
+
+ ${stats
+ .map(
+ (item) => `
+
+
+ ${escapeHtml(item.label)}
+ ${escapeHtml(item.trend)}
+
+ ${escapeHtml(item.value)}
+ ${escapeHtml(item.hint)}
+
+ `,
+ )
+ .join("")}
+
+
+ ${panelHeader("Quick actions", "Jump into the common flows without digging through nested screens.")}
+
+
+
+
+ ${panelHeader("Recent orders", "A compact snapshot of the latest patient orders and their state.", '
View all ')}
+
+
+
+ Patient Order Status Updated
+
+
+ ${mockOrders
+ .map(
+ (order) => `
+
+ ${escapeHtml(order.patient)} ${escapeHtml(order.mode)}
+ ${escapeHtml(order.id)} ${escapeHtml(order.diagnosis)}
+ ${statusBadge(order.status)}
+ ${escapeHtml(order.updated)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+
+ ${panelHeader("Today’s notes", "Items that need attention now, not later.")}
+
+ ${[
+ ["FPP ready for release", "Review Hematology group before rounding."],
+ ["Pending sample", "One fasting sample still waiting in the queue."],
+ ["Password rotation", "Prompt user to refresh credentials after 90 days."],
+ ]
+ .map(
+ ([title, text]) => `
+
+
+
${escapeHtml(title)}
+
${escapeHtml(text)}
+
+
${icon("arrow")}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+ `;
+}
+
+function renderOrdersTable(orders, selectedOrderId, filter = "All") {
+ const selected = orders.find((item) => item.id === selectedOrderId) || orders[0] || mockOrders[0];
+ return `
+
+
+ ${panelHeader("Search orders", "Use the filter to match the old app flow without giving up desktop readability.", 'Create order ')}
+
+
+
+
+ Patient Order ID Doctor Status Updated
+
+
+ ${orders
+ .map(
+ (order) => `
+
+ ${escapeHtml(order.patient)} ${escapeHtml(order.mode)}
+ ${escapeHtml(order.id)}
+ ${escapeHtml(order.doctor)}
+ ${statusBadge(order.status)}
+ ${escapeHtml(order.updated)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+ ${orders
+ .map(
+ (order) => `
+
+
+
+
${escapeHtml(order.patient)}
+
${escapeHtml(order.id)} · ${escapeHtml(order.mode)}
+
+ ${statusBadge(order.status)}
+
+
+ ${escapeHtml(order.doctor)}
+ ${escapeHtml(order.updated)}
+
+
+ `,
+ )
+ .join("")}
+
+
+
+ ${panelHeader("Selected order", "Master-detail layout keeps context visible while reviewing the list.", `Open detail `)}
+
+
+
${escapeHtml(selected.patient)}
+
${escapeHtml(selected.id)} · ${escapeHtml(selected.mode)}
+
+ ${statusBadge(selected.status)}
+ ${escapeHtml(selected.age)} years
+ ${escapeHtml(selected.gender)}
+
+
+
+
Diagnosis ${escapeHtml(selected.diagnosis)}
+
Requested tests ${escapeHtml(selected.tests.join(", "))}
+
Special note ${escapeHtml(selected.message)}
+
+
+
+
+ `;
+}
+
+function renderOrderDetail(order) {
+ return `
+
+
+ ${panelHeader(`${order.patient} · ${order.id}`, "Order detail with the same structure as the proposed master-detail workflow.", `Pesan khusus `)}
+
+
Status ${statusBadge(order.status)}
+
Patient ${escapeHtml(order.patient)} ${escapeHtml(order.mode)} · ${escapeHtml(order.age)} years
+
Doctor ${escapeHtml(order.doctor)} ${escapeHtml(mockUser.hospital)}
+
+
+
+
+ ${panelHeader("Requested tests", "List/table treatment on desktop, cards on smaller screens.")}
+
+ ${order.tests
+ .map(
+ (test) => `
+
${escapeHtml(test)} Included in the current order bundle.
+ `,
+ )
+ .join("")}
+
+
+
+ ${panelHeader("Clinical note", "A compact summary for quick review.")}
+ ${escapeHtml(order.message)}
+ ${order.apiSaran ? `
${escapeHtml(typeof order.apiSaran === "string" ? order.apiSaran : JSON.stringify(order.apiSaran))}
` : ""}
+
+
+
Diagnosis ${escapeHtml(order.diagnosis)}
+
Updated ${escapeHtml(order.updated)}
+
+
+
+
+ `;
+}
+
+function renderOrderForm(step, stepKey = "demografi") {
+ const steps = [
+ ["demografi", "Demografi", "Patient identity and contact details."],
+ ["diagnosa", "Diagnosa", "Clinical indication and diagnosis."],
+ ["pemeriksaan", "Pemeriksaan", "Choose examination groups."],
+ ["qrcode", "QR Code", "Fast entry for existing records."],
+ ["review", "Review", "Check before submit."],
+ ];
+ const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey));
+ const stepBody = {
+ demografi: `
+
+ Patient name
+ Medical record no.
+ Gender Female Male
+ Date of birth
+ Address
+
+ `,
+ diagnosa: `
+
+ Diagnosis
+ Patient note
+ Clinical complaint
+
+ `,
+ pemeriksaan: `
+
+ ${["Hematology", "Clinical Chemistry", "Urinalysis", "Immunology", "Microbiology"].map((item) => `${escapeHtml(item)} `).join("")}
+
+
+
+ ${["CBC", "Glucose", "Lipid Profile", "CRP"].map((item) => `
+
+ ${escapeHtml(item)}
+ Selectable test item
+
+ `).join("")}
+
+ `,
+ qrcode: `
+
+
+
Scan existing patient QR
+
Use a QR code to pull patient and visit context instantly.
+
QR preview area
+
+
+
Manual fallback
+
Still allow manual entry if the scan is unavailable.
+
QR token
+
+
+ `,
+ review: `
+
+
Summary All steps are merged into a final review before submit.
+
+
+
+
+
Message Prioritize fasting sample
+
+
+ `,
+ }[stepKey];
+ const prev = steps[activeIndex - 1];
+ const next = steps[activeIndex + 1];
+ return `
+
+ ${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", 'Cancel ')}
+
+
+
+ `;
+}
+
+function renderResultsTable(results, selectedResultId) {
+ const selected = results.find((item) => item.id === selectedResultId) || results[0] || mockResults[0];
+ return `
+
+
+ ${panelHeader("Result history", "Desktop shows table detail. Mobile collapses into stacked cards.")}
+
+
+
+
+ Patient Result ID Test Status Date
+
+
+ ${results
+ .map(
+ (result) => `
+
+ ${escapeHtml(result.patient)} ${escapeHtml(result.summary)}
+ ${escapeHtml(result.id)}
+ ${escapeHtml(result.test)}
+ ${statusBadge(result.status)}
+ ${escapeHtml(result.date)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+ ${results
+ .map(
+ (result) => `
+
+
+
+
${escapeHtml(result.patient)}
+
${escapeHtml(result.test)} · ${escapeHtml(result.date)}
+
+ ${statusBadge(result.status)}
+
+ ${escapeHtml(result.summary)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+ `;
+}
+
+function renderResultDetail(result) {
+ return `
+
+ ${panelHeader(`${result.patient} · ${result.id}`, "Result detail with summary, status, and interpretation fields.", 'Back to results ')}
+
+
Status ${statusBadge(result.status)}
+
Test ${escapeHtml(result.test)}
+
Value ${escapeHtml(result.value)}
+
+
+
+
+
${escapeHtml(result.summary)}
+
+
+
+
Date ${escapeHtml(result.date)}
+
Interpretation Borderline value requires doctor review.
+
Action Keep in pending state until confirmed.
+
+
+
+
+ `;
+}
+
+function renderFpp(groups) {
+ return `
+
+
+ ${panelHeader("FPP catalog", "Filter blocks and list cards on mobile, richer panel on desktop.")}
+
+ ${["All", ...mockFppGroups.map((item) => item.group)]
+ .map((item) => `
${escapeHtml(item)} `)
+ .join("")}
+
+
+
+ ${groups.items
+ .map(
+ (item) => `
+
+
+
+ ${["Filter", "Inspect", "Select", "Export"]
+ .map(
+ (action) => `
+
+
${escapeHtml(action)}
+
Workflow action for ${escapeHtml(item.group)}.
+
+ `,
+ )
+ .join("")}
+
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+}
+
+function renderPatients() {
+ return `
+
+
+ ${panelHeader("Patient registration", "Landing page for registration, lookup, and entry flow.")}
+
+
+
New patient
+
Open the registration stepper and start a fresh case.
+
Start registration
+
+
+
Lookup
+
Search an existing patient and reuse their visit data.
+
Search patient
+
+
+
QR entry
+
Fast path for scan-based intake.
+
Open QR flow
+
+
+
+
+ ${panelHeader("Recent patients", "A compact list that becomes cards on mobile.")}
+
+
+
+ Name MRN Gender Last visit Note
+
+
+ ${mockPatients
+ .map(
+ (person) => `
+
+ ${escapeHtml(person.name)}
+ ${escapeHtml(person.mrn)}
+ ${escapeHtml(person.gender)}
+ ${escapeHtml(person.lastVisit)}
+ ${escapeHtml(person.note)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+
+ `;
+}
+
+function renderSettings() {
+ return `
+
+
+ ${panelHeader("Account", "Profile data and security entry points.")}
+
+
Name ${escapeHtml(mockUser.name)}
+
Doctor ID ${escapeHtml(mockUser.doctorId)}
+
Role ${escapeHtml(mockUser.role)}
+
Hospital ${escapeHtml(mockUser.hospital)}
+
+
+
+ ${panelHeader("Settings actions", "Keep the common account actions obvious.")}
+
+
+
+ `;
+}
+
+function renderChangePassword() {
+ return `
+
+ ${panelHeader("Change password", "Inline validation and a straightforward submit path.")}
+
+
+ `;
+}
+
+function loginPage({ error = "" } = {}) {
+ return layout(
+ "DocLink Login",
+ `
+
+
+
+
DocLink rebuild · responsive shell
+
Clinical workflow that works on desktop and mobile.
+
The old app behavior is preserved in a cleaner layout with dashboard, orders, results, FPP, and settings routes.
+
+
Route structure Login, dashboard, order flow, result flow, and account pages.
+
Responsive shell Persistent sidebar on desktop, bottom nav on mobile.
+
Clean interaction model HTMX fragments plus server-rendered templates.
+
+
+
+
+
+ `,
+ { authenticated: false, shell: false, activePath: "/login" },
+ );
+}
+
+function splashPage() {
+ return layout(
+ "DocLink Splash",
+ `
+
+
+
+
Starting DocLink
+
Loading workspace...
+
Checking session and routing into the right entry point.
+
+
+
+
+ `,
+ { authenticated: false, shell: false, activePath: "/splash" },
+ );
+}
+
+function problemLoginPage() {
+ return layout(
+ "Problem Login",
+ `
+
+
+
+
Login problem
+
Access needs attention.
+
Use this page when auth state is broken or a session expires.
+
+
+
+
+ `,
+ { authenticated: false, shell: false, activePath: "/problem-login" },
+ );
+}
+
+function orderNewPage(path) {
+ const step = path.split("/").filter(Boolean)[2] || "demografi";
+ return layout("Create Order", renderOrderForm({}, step), {
+ authenticated: true,
+ activePath: "/orders",
+ subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
+ });
+}
+
+function ordersPage({ query = {}, orders = mockOrders, selectedOrderId = mockOrders[0].id } = {}) {
+ return layout("Orders", `${renderOrdersTable(orders, selectedOrderId, query.status || "All")}
`, {
+ authenticated: true,
+ activePath: "/orders",
+ });
+}
+
+function resultsPage({ query = {}, results = mockResults, selectedResultId = mockResults[0].id } = {}) {
+ return layout("Results", `${renderResultsTable(results, selectedResultId)}
`, {
+ authenticated: true,
+ activePath: "/results",
+ });
+}
+
+function fppPage({ group = "All", groups = mockFppGroups } = {}) {
+ return layout("FPP", `${renderFpp({ items: groups, filter: group })}
`, {
+ authenticated: true,
+ activePath: "/fpp",
+ });
+}
+
+function patientsPage() {
+ return layout("Patients", renderPatients(), { authenticated: true, activePath: "/patients" });
+}
+
+function settingsPage() {
+ return layout("Settings", renderSettings(), { authenticated: true, activePath: "/settings" });
+}
+
+function changePasswordPage() {
+ return layout("Change Password", renderChangePassword(), {
+ authenticated: true,
+ activePath: "/settings/change-password",
+ });
+}
+
+function orderDetailPage(order) {
+ return layout("Order Detail", renderOrderDetail(order), {
+ authenticated: true,
+ activePath: "/orders",
+ });
+}
+
+function resultDetailPage(result) {
+ return layout("Result Detail", renderResultDetail(result), {
+ authenticated: true,
+ activePath: "/results",
+ });
+}
+
+function specialMessagePage(order) {
+ return layout(
+ "Pesan Khusus",
+ `
+
+ ${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.", `Back `)}
+
+
+
${escapeHtml(order.patient)}
+
${escapeHtml(order.id)}
+
${statusBadge(order.status)}
+
${escapeHtml(order.message)}
+ ${order.apiSaran ? `
${escapeHtml(typeof order.apiSaran === "string" ? order.apiSaran : JSON.stringify(order.apiSaran))}
` : ""}
+
+
+
+
+ `,
+ { authenticated: true, activePath: "/orders" },
+ );
+}
+
+function emptyRoute(path) {
+ return layout("Not Found", `Route not found ${escapeHtml(path)} is not part of the current rebuild scope.
Go to dashboard `, {
+ authenticated: true,
+ activePath: "/",
+ });
+}
+
+function cookieHeader(req) {
+ return req.headers.cookie || "";
+}
+
+function getCookie(req, name) {
+ const cookies = cookieHeader(req)
+ .split(";")
+ .map((part) => part.trim())
+ .filter(Boolean);
+ for (const item of cookies) {
+ const [key, ...rest] = item.split("=");
+ if (key === name) return decodeURIComponent(rest.join("="));
+ }
+ return "";
+}
+
+function setCookie(name, value, opts = {}) {
+ const parts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "HttpOnly", "SameSite=Lax"];
+ if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);
+ if (opts.secure) parts.push("Secure");
+ return parts.join("; ");
+}
+
+function deleteCookie(name) {
+ return `${name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+}
+
+function readSession(req) {
+ const raw = getCookie(req, sessionKey);
+ if (!raw) return null;
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return null;
+ }
+}
+
+function requireAuth(req, res) {
+ const session = readSession(req);
+ if (session) return session;
+ redirect(res, "/login");
+ return null;
+}
+
+async function fetchJson(url, options = {}) {
+ const response = await fetch(url, options);
+ const contentType = response.headers.get("content-type") || "";
+ const body = contentType.includes("application/json") ? await response.json() : await response.text();
+ if (!response.ok) {
+ const error = new Error(`Upstream error ${response.status}`);
+ error.status = response.status;
+ error.body = body;
+ throw error;
+ }
+ return body;
+}
+
+async function apiPost(path, payload, token = "") {
+ const headers = { "Content-Type": "application/json" };
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ payload.token = payload.token || token;
+ }
+ const url = `${API_BASE}${path}`;
+ return fetchJson(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(payload),
+ });
+}
+
+function normalizeSession(payload) {
+ const data = payload?.data || payload?.result || payload;
+ const token = data?.token || data?.access_token || data?.accessToken || payload?.token || "";
+ const username = data?.username || data?.M_UserUsername || data?.name || "";
+ const doctorId = data?.doctor_id || data?.M_UserID || data?.doctorId || "";
+ return {
+ token,
+ username,
+ doctorId,
+ raw: payload,
+ };
+}
+
+function extractArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (!payload || typeof payload !== "object") return null;
+ for (const key of ["data", "result", "results", "items", "list"]) {
+ const value = payload[key];
+ const found = extractArray(value);
+ if (found) return found;
+ }
+ return null;
+}
+
+function normalizeOrder(raw, index = 0) {
+ const testsSource = raw?.details || raw?.tests || raw?.items || raw?.order_details || [];
+ const tests = Array.isArray(testsSource)
+ ? testsSource
+ .map((item) => {
+ if (typeof item === "string") return item;
+ return item?.name || item?.test_name || item?.detail_name || item?.exam || item?.label || "";
+ })
+ .filter(Boolean)
+ : [];
+ const status = raw?.status || raw?.order_status || raw?.state || "Processing";
+ return {
+ id:
+ raw?.order_patient_id ||
+ raw?.order_id ||
+ raw?.id ||
+ raw?.OrderPatientID ||
+ raw?.OrderID ||
+ `ORD-${String(index + 1).padStart(5, "0")}`,
+ patient: raw?.patient_name || raw?.name || raw?.patient || raw?.patient_fullname || "Patient",
+ doctor: raw?.doctor_name || raw?.doctor || raw?.M_UserUsername || mockUser.name,
+ updated: raw?.updated_at || raw?.updated || raw?.created_at || "Today",
+ status,
+ tone: statusClass(status),
+ mode: raw?.mode || raw?.visit_type || raw?.patient_type || "Outpatient",
+ age: String(raw?.age || raw?.patient_age || ""),
+ gender: raw?.gender || raw?.patient_gender || "",
+ tests,
+ diagnosis: raw?.patient_diagnosa || raw?.diagnosis || raw?.note || "",
+ message: raw?.message || raw?.patient_note || "",
+ };
+}
+
+function normalizeResult(raw, index = 0) {
+ const status = raw?.status || raw?.result_status || "Released";
+ return {
+ id:
+ raw?.result_id ||
+ raw?.order_id ||
+ raw?.id ||
+ raw?.hasil_id ||
+ `RES-${String(index + 1).padStart(4, "0")}`,
+ patient: raw?.patient_name || raw?.name || raw?.patient || "Patient",
+ test: raw?.test_name || raw?.item_name || raw?.test || "Result",
+ status,
+ tone: statusClass(status),
+ date: raw?.date || raw?.created_at || raw?.updated_at || "Today",
+ summary: raw?.summary || raw?.note || raw?.result_summary || "",
+ value: raw?.value || raw?.result_value || raw?.result || "",
+ };
+}
+
+async function loadOrders(session, search = "", status = "All") {
+ const term = String(search || "").trim();
+ try {
+ const payload = await apiPost(
+ "/order/search_order_pasien_by_doktorid",
+ {
+ token: session.token,
+ OrderPatientM_DoctorID: session.doctorId || mockUser.doctorId,
+ search: term,
+ },
+ session.token,
+ );
+ const rows = extractArray(payload) || [];
+ const orders = rows.map((row, index) => normalizeOrder(row, index)).filter((item) => {
+ const matchesSearch =
+ !term ||
+ [item.id, item.patient, item.status, item.diagnosis, item.message].some((value) =>
+ String(value).toLowerCase().includes(term.toLowerCase()),
+ );
+ const matchesStatus = !status || status === "All" || item.status === status;
+ return matchesSearch && matchesStatus;
+ });
+ if (orders.length) return orders;
+ } catch {
+ // Fall through to mock data.
+ }
+ return filterOrders(term, status);
+}
+
+async function loadResults(session, search = "") {
+ const term = String(search || "").trim();
+ try {
+ const payload = await apiPost(
+ "/order/hasil_belum_keluar_by_id",
+ { token: session.token, order_id: session.orderId || "" },
+ session.token,
+ );
+ const rows = extractArray(payload) || [];
+ const results = rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
+ if (!term) return true;
+ return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
+ String(value).toLowerCase().includes(term.toLowerCase()),
+ );
+ });
+ if (results.length) return results;
+ } catch {
+ // Fall through to mock data.
+ }
+ return filterResults(term);
+}
+
+async function loadFpp(session, group = "All") {
+ try {
+ const payload = await apiPost("/Fpp/load/1/1", { token: session.token }, session.token);
+ const rows = extractArray(payload) || [];
+ const items = rows.map((row, index) => ({
+ group: row?.group || row?.kategori || row?.name || `GROUP-${index + 1}`,
+ count: Number(row?.count || row?.total || row?.qty || 1),
+ desc: row?.description || row?.desc || "Laboratory reference item.",
+ }));
+ if (items.length) {
+ const filtered = group === "All" ? items : items.filter((item) => item.group === group);
+ return { items: filtered, filter: group };
+ }
+ } catch {
+ // Fall through to mock data.
+ }
+ const items = group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group);
+ return { items, filter: group };
+}
+
+async function loadOrderDetail(session, orderId) {
+ const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
+ const detail = { ...order };
+ try {
+ const saran = await apiPost(
+ "/order/get_order_saran_by_order_patient_id",
+ { token: session.token, order_patient_id: orderId },
+ session.token,
+ );
+ detail.apiSaran = extractArray(saran) || saran?.message || saran?.note || saran?.result || "";
+ } catch {
+ detail.apiSaran = "";
+ }
+ try {
+ const hasil = await apiPost(
+ "/order/hasil_belum_keluar_by_id",
+ { token: session.token, order_id: orderId },
+ session.token,
+ );
+ detail.apiHasil = extractArray(hasil) || hasil?.message || hasil?.note || hasil?.result || "";
+ } catch {
+ detail.apiHasil = "";
+ }
+ return detail;
+}
+
+function json(res, status, payload) {
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
+ res.end(JSON.stringify(payload));
+}
+
+function redirect(res, location, headers = {}) {
+ res.writeHead(302, { Location: location, ...headers });
+ res.end();
+}
+
+function html(res, status, body, headers = {}) {
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers });
+ res.end(body);
+}
+
+function isHtmx(req) {
+ return req.headers["hx-request"] === "true";
+}
+
+async function readBody(req) {
+ const chunks = [];
+ for await (const chunk of req) chunks.push(chunk);
+ const raw = Buffer.concat(chunks).toString("utf8");
+ const contentType = req.headers["content-type"] || "";
+ if (contentType.includes("application/json")) return raw ? JSON.parse(raw) : {};
+ return Object.fromEntries(new URLSearchParams(raw));
+}
+
+function filterOrders(search, status) {
+ const term = String(search || "").trim().toLowerCase();
+ return mockOrders.filter((order) => {
+ const matchesSearch =
+ !term ||
+ [order.id, order.patient, order.status, order.diagnosis, order.message].some((value) =>
+ String(value).toLowerCase().includes(term),
+ );
+ const matchesStatus = !status || status === "All" || order.status === status;
+ return matchesSearch && matchesStatus;
+ });
+}
+
+function filterResults(search) {
+ const term = String(search || "").trim().toLowerCase();
+ return mockResults.filter((result) => {
+ if (!term) return true;
+ return [result.id, result.patient, result.test, result.status, result.summary].some((value) =>
+ String(value).toLowerCase().includes(term),
+ );
+ });
+}
+
+function fragmentOrdersTable(search = "", status = "All", ordersData = null) {
+ const orders = ordersData || filterOrders(search, status);
+ const selected = orders[0] || mockOrders[0];
+ return `
+
+
+ ${panelHeader("Search orders", "Filter the list without full page refresh.")}
+
+
+ ${["All", "Processing", "Ready", "Needs review"]
+ .map(
+ (item) => `
+ ${escapeHtml(item)}
+ `,
+ )
+ .join("")}
+
+
+
+
+ Patient Order ID Doctor Status Updated
+
+
+ ${orders
+ .map(
+ (order) => `
+
+ ${escapeHtml(order.patient)} ${escapeHtml(order.mode)}
+ ${escapeHtml(order.id)}
+ ${escapeHtml(order.doctor)}
+ ${statusBadge(order.status)}
+ ${escapeHtml(order.updated)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+
+ ${panelHeader("Selected order", "Context stays visible while you review the table.", `Open detail `)}
+
+
+
${escapeHtml(selected.patient)}
+
${escapeHtml(selected.id)} · ${escapeHtml(selected.mode)}
+
${statusBadge(selected.status)}${escapeHtml(selected.age)} years ${escapeHtml(selected.gender)}
+
+
+
Diagnosis ${escapeHtml(selected.diagnosis)}
+
Requested tests ${escapeHtml(selected.tests.join(", "))}
+
Special note ${escapeHtml(selected.message)}
+
+
+
+
+ `;
+}
+
+function fragmentResultsTable(search = "", resultsData = null) {
+ const results = resultsData || filterResults(search);
+ const selected = results[0] || mockResults[0];
+ return `
+
+
+ ${panelHeader("Result history", "Desktop shows table detail. Mobile collapses into stacked cards.")}
+
+
+
+
+ Patient Result ID Test Status Date
+
+
+ ${results
+ .map(
+ (result) => `
+
+ ${escapeHtml(result.patient)} ${escapeHtml(result.summary)}
+ ${escapeHtml(result.id)}
+ ${escapeHtml(result.test)}
+ ${statusBadge(result.status)}
+ ${escapeHtml(result.date)}
+
+ `,
+ )
+ .join("")}
+
+
+
+
+
+
+ `;
+}
+
+function fragmentFpp(group = "All", itemsData = null) {
+ const items = itemsData || (group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group));
+ return `
+
+
+ ${panelHeader("FPP catalog", "Filter blocks and list cards on mobile, richer panel on desktop.")}
+
+ ${["All", ...mockFppGroups.map((item) => item.group)]
+ .map(
+ (item) => `
+
${escapeHtml(item)}
+ `,
+ )
+ .join("")}
+
+
+
+ ${items
+ .map(
+ (item) => `
+
+
+
+ ${["Filter", "Inspect", "Select", "Export"]
+ .map(
+ (action) => `
+
+
${escapeHtml(action)}
+
Workflow action for ${escapeHtml(item.group)}.
+
+ `,
+ )
+ .join("")}
+
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+}
+
+function fragmentOrderStep(step) {
+ return renderOrderForm({}, step);
+}
+
+function fragmentPesanKhusus(orderId) {
+ const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
+ return `
+
+ ${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.", `Back `)}
+
+
+
${escapeHtml(order.patient)}
+
${escapeHtml(order.id)}
+
${statusBadge(order.status)}
+
${escapeHtml(order.message)}
+
+
+
+ Special message
+ ${escapeHtml(order.message)}
+
+
+
+
+
+ `;
+}
+
+async function renderRoute(req, res, url) {
+ const path = url.pathname;
+ const query = Object.fromEntries(url.searchParams.entries());
+ const session = readSession(req);
+ const authed = Boolean(session);
+ const isGet = req.method === "GET" || req.method === "HEAD";
+
+ if (path === "/styles.css") {
+ const css = await readFile(new URL("./styles.css", import.meta.url), "utf8");
+ res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
+ res.end(css);
+ return;
+ }
+
+ if (path === "/login" && isGet) {
+ html(res, 200, loginPage(), { "Cache-Control": "no-store" });
+ return;
+ }
+
+ if (path === "/splash" && isGet) {
+ html(res, 200, splashPage(), { "Cache-Control": "no-store" });
+ return;
+ }
+
+ if (path === "/problem-login" && isGet) {
+ html(res, 200, problemLoginPage(), { "Cache-Control": "no-store" });
+ return;
+ }
+
+ if (path === "/login" && req.method === "POST") {
+ const body = await readBody(req);
+ try {
+ const payload = await apiPost("/auth/login", body);
+ const normalized = normalizeSession(payload);
+ const upstreamFailed = String(payload?.status || payload?.result_status || "").toUpperCase() === "ERR" || !normalized.token;
+ if (upstreamFailed) {
+ throw new Error(payload?.message || "Invalid login response");
+ }
+ const sessionValue = JSON.stringify({
+ token: normalized.token,
+ username: normalized.username || body.username || "",
+ doctorId: normalized.doctorId || body.doctor_id || "",
+ raw: payload,
+ });
+ redirect(res, "/", {
+ "Set-Cookie": setCookie(sessionKey, sessionValue, { maxAge: 60 * 60 * 12 }),
+ });
+ } catch (error) {
+ if (process.env.DOCLINK_DEMO_MODE !== "0") {
+ const sessionValue = JSON.stringify({
+ token: "demo-token",
+ username: body.username || mockUser.name,
+ doctorId: body.doctor_id || mockUser.doctorId,
+ raw: { demo: true },
+ });
+ redirect(res, "/", {
+ "Set-Cookie": setCookie(sessionKey, sessionValue, { maxAge: 60 * 60 * 12 }),
+ });
+ } else {
+ html(res, 200, loginPage({ error: "Login failed or upstream API unavailable. Check credentials and network." }));
+ }
+ }
+ return;
+ }
+
+ if (path === "/logout" && req.method === "POST") {
+ const sessionData = readSession(req);
+ if (sessionData?.token) {
+ try {
+ await apiPost(
+ "/auth/logout",
+ {
+ M_UserID: sessionData.doctorId || mockUser.doctorId,
+ M_UserUsername: sessionData.username || mockUser.name,
+ },
+ sessionData.token,
+ );
+ } catch {
+ // Ignore upstream logout failures and still clear local session.
+ }
+ }
+ redirect(res, "/login", { "Set-Cookie": deleteCookie(sessionKey) });
+ return;
+ }
+
+ if (path === "/settings/change-password" && req.method === "POST") {
+ const sessionData = requireAuth(req, res);
+ if (!sessionData) return;
+ const body = await readBody(req);
+ try {
+ await apiPost(
+ "/auth/change_password",
+ {
+ token: sessionData.token,
+ M_UserID: sessionData.doctorId || mockUser.doctorId,
+ username: sessionData.username || mockUser.name,
+ doctor_id: sessionData.doctorId || mockUser.doctorId,
+ new_password: body.new_password,
+ confirm_password: body.confirm_password,
+ },
+ sessionData.token,
+ );
+ redirect(res, "/settings");
+ } catch (error) {
+ html(
+ res,
+ 200,
+ layout(
+ "Change Password",
+ `${panelHeader("Change password", "Inline validation and a straightforward submit path.")}Upstream change password call failed. Check API availability.
`,
+ { authenticated: true, activePath: "/settings/change-password" },
+ ),
+ );
+ }
+ return;
+ }
+
+ if (path === "/orders/new" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, orderNewPage(path));
+ return;
+ }
+
+ if (path.startsWith("/orders/new/") && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, orderNewPage(path));
+ return;
+ }
+
+ if (path === "/orders" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const orders = await loadOrders(session, query.search || "", query.status || "All");
+ html(res, 200, ordersPage({ query, orders, selectedOrderId: orders[0]?.id || mockOrders[0].id }));
+ return;
+ }
+
+ if (path === "/results" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const results = await loadResults(session, query.search || "");
+ html(res, 200, resultsPage({ query, results, selectedResultId: results[0]?.id || mockResults[0].id }));
+ return;
+ }
+
+ if (path === "/results/historical" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(
+ res,
+ 200,
+ layout(
+ "Historical results",
+ `${panelHeader("Historical results", "A compact historic view that still fits the responsive shell.")} `,
+ { authenticated: true, activePath: "/results" },
+ ),
+ );
+ return;
+ }
+
+ if (path === "/results/pending" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(
+ res,
+ 200,
+ layout(
+ "Pending results",
+ `${panelHeader("Pending results", "Items that need attention before release.")}${mockResults.filter((item) => item.status !== "Released").map((item) => `
${escapeHtml(item.patient)} ${statusBadge(item.status)}
${escapeHtml(item.test)} · ${escapeHtml(item.summary)}
Open detail `).join("")}
`,
+ { authenticated: true, activePath: "/results" },
+ ),
+ );
+ return;
+ }
+
+ if (path === "/fpp" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const groups = await loadFpp(session, query.group || "All");
+ html(res, 200, fppPage({ group: groups.filter, groups: groups.items }));
+ return;
+ }
+
+ if (path === "/patients" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, patientsPage());
+ return;
+ }
+
+ if ((path === "/settings" || path === "/settings/account") && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, settingsPage());
+ return;
+ }
+
+ if (path === "/settings/change-password" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, changePasswordPage());
+ return;
+ }
+
+ if (path === "/" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, layout("Dashboard", dashboardPage(), { authenticated: true, activePath: "/" }));
+ return;
+ }
+
+ if (path.startsWith("/orders/") && path.endsWith("/pesan-khusus") && isGet) {
+ if (!requireAuth(req, res)) return;
+ const orderId = path.split("/")[2];
+ const order = await loadOrderDetail(session, orderId);
+ html(res, 200, specialMessagePage(order));
+ return;
+ }
+
+ if (path.startsWith("/orders/") && isGet) {
+ if (!requireAuth(req, res)) return;
+ const orderId = path.split("/")[2];
+ const order = await loadOrderDetail(session, orderId);
+ html(res, 200, orderDetailPage(order));
+ return;
+ }
+
+ if (path.startsWith("/results/") && isGet) {
+ if (!requireAuth(req, res)) return;
+ const resultId = path.split("/")[2];
+ const results = await loadResults(session, query.search || "");
+ const result = results.find((item) => item.id === resultId) || mockResults.find((item) => item.id === resultId) || mockResults[0];
+ html(res, 200, resultDetailPage(result));
+ return;
+ }
+
+ if (path === "/fragments/orders/table" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const orders = await loadOrders(session, query.search || "", query.status || "All");
+ html(res, 200, fragmentOrdersTable(query.search || "", query.status || "All", orders), { "HX-Trigger": JSON.stringify({ "doclink:orders-updated": true }) });
+ return;
+ }
+
+ if (path === "/fragments/results/table" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const results = await loadResults(session, query.search || "");
+ html(res, 200, fragmentResultsTable(query.search || "", results));
+ return;
+ }
+
+ if (path === "/fragments/fpp/list" && isGet) {
+ if (!requireAuth(req, res)) return;
+ const groups = await loadFpp(session, query.group || "All");
+ html(res, 200, fragmentFpp(groups.filter || "All", groups.items));
+ return;
+ }
+
+ if (path.startsWith("/fragments/forms/order-step/") && isGet) {
+ if (!requireAuth(req, res)) return;
+ const step = path.split("/")[4] || "demografi";
+ html(res, 200, fragmentOrderStep(step));
+ return;
+ }
+
+ if (path === "/fragments/modals/pesan-khusus" && isGet) {
+ if (!requireAuth(req, res)) return;
+ html(res, 200, fragmentPesanKhusus(query.order_id || mockOrders[0].id));
+ return;
+ }
+
+ if (path.startsWith("/api/") && isGet) {
+ json(res, 200, { ok: true });
+ return;
+ }
+
+ if (path === "/orders" && req.method === "POST") {
+ const sessionData = requireAuth(req, res);
+ if (!sessionData) return;
+ const body = await readBody(req);
+ try {
+ await apiPost(
+ "/order/order_patient",
+ {
+ token: sessionData.token,
+ M_MouID: body.M_MouID || mockUser.doctorId,
+ patient_name: body.patient_name || "",
+ patient_diagnosa: body.patient_diagnosa || "",
+ patient_address: body.patient_address || "",
+ patient_nik: body.patient_nik || "",
+ patient_hp: body.patient_hp || "",
+ patient_dob: body.patient_dob || "",
+ patient_note: body.patient_note || "",
+ details: body.details || [],
+ },
+ sessionData.token,
+ );
+ redirect(res, "/orders");
+ } catch {
+ html(
+ res,
+ 200,
+ layout(
+ "Create Order",
+ `${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.")}Upstream order create call failed. Check API availability.
`,
+ { authenticated: true, activePath: "/orders" },
+ ),
+ );
+ }
+ return;
+ }
+
+ if (path.startsWith("/orders/") && path.endsWith("/pesan-khusus") && req.method === "POST") {
+ const sessionData = requireAuth(req, res);
+ if (!sessionData) return;
+ const orderId = path.split("/")[2];
+ const body = await readBody(req);
+ try {
+ await apiPost(
+ "/Pesankhusus/add_pesan_khusus",
+ {
+ token: sessionData.token,
+ order_id: orderId,
+ pesan_khusus: body.pesan_khusus,
+ },
+ sessionData.token,
+ );
+ redirect(res, `/orders/${orderId}`);
+ } catch {
+ html(
+ res,
+ 200,
+ layout(
+ "Pesan Khusus",
+ `${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.")}Upstream save failed. Check API availability.
`,
+ { authenticated: true, activePath: "/orders" },
+ ),
+ );
+ }
+ return;
+ }
+
+ html(res, 404, emptyRoute(path));
+}
+
+const server = http.createServer(async (req, res) => {
+ try {
+ const url = new URL(req.url, `http://${req.headers.host}`);
+ await renderRoute(req, res, url);
+ } catch (error) {
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
+ res.end(`Internal error: ${error.message}`);
+ }
+});
+
+server.listen(PORT, () => {
+ console.log(`DocLink Web running on http://localhost:${PORT}`);
+});
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..67031c1
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,918 @@
+:root {
+ color-scheme: light;
+ --bg: #f4f5f7;
+ --bg-soft: #eef2f5;
+ --surface: rgba(255, 255, 255, 0.82);
+ --surface-strong: #ffffff;
+ --line: rgba(15, 23, 42, 0.1);
+ --text: #122033;
+ --muted: #5b6677;
+ --muted-2: #7c8797;
+ --brand: #0f766e;
+ --brand-strong: #115e59;
+ --accent: #d97706;
+ --accent-soft: #fff4e6;
+ --success: #15803d;
+ --warning: #b45309;
+ --danger: #b91c1c;
+ --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.14);
+ --shadow-md: 0 12px 28px rgba(15, 23, 42, 0.12);
+ --radius-xl: 28px;
+ --radius-lg: 22px;
+ --radius-md: 16px;
+ --radius-sm: 12px;
+ --nav-width: 290px;
+ --content-max: 1440px;
+ --gap: 22px;
+ --font: "Avenir Next", "Nunito Sans", "Segoe UI", system-ui, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ min-height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: var(--font);
+ color: var(--text);
+ background:
+ radial-gradient(circle at top left, rgba(15, 118, 110, 0.08), transparent 28%),
+ radial-gradient(circle at bottom right, rgba(217, 119, 6, 0.07), transparent 28%),
+ linear-gradient(180deg, #f8fafc 0%, var(--bg) 100%);
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+.bg-orb,
+.bg-grid {
+ pointer-events: none;
+ position: fixed;
+ inset: auto;
+ z-index: 0;
+}
+
+.bg-orb {
+ width: 28rem;
+ height: 28rem;
+ border-radius: 999px;
+ filter: blur(32px);
+ opacity: 0.42;
+}
+
+.orb-one {
+ top: -9rem;
+ left: -8rem;
+ background: rgba(15, 118, 110, 0.18);
+}
+
+.orb-two {
+ right: -10rem;
+ bottom: 3rem;
+ background: rgba(217, 119, 6, 0.14);
+}
+
+.bg-grid {
+ inset: 0;
+ opacity: 0.26;
+ background-image:
+ linear-gradient(rgba(15, 23, 42, 0.04) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(15, 23, 42, 0.04) 1px, transparent 1px);
+ background-size: 42px 42px;
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 88%);
+}
+
+#app {
+ position: relative;
+ z-index: 1;
+ min-height: 100vh;
+}
+
+.page-shell {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: var(--nav-width) minmax(0, 1fr);
+}
+
+.sidebar {
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ padding: 18px;
+ border-right: 1px solid var(--line);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.72));
+ backdrop-filter: blur(22px);
+}
+
+.sidebar-card {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ padding: 20px;
+ border-radius: var(--radius-xl);
+ background: var(--surface);
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ box-shadow: var(--shadow-lg);
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.brand-mark {
+ width: 46px;
+ height: 46px;
+ border-radius: 16px;
+ display: grid;
+ place-items: center;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ color: white;
+ background:
+ radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.3), transparent 38%),
+ linear-gradient(135deg, var(--brand), var(--brand-strong));
+ box-shadow: 0 14px 24px rgba(15, 118, 110, 0.26);
+}
+
+.brand-text strong {
+ display: block;
+ font-size: 1rem;
+ line-height: 1.1;
+}
+
+.brand-text span {
+ display: block;
+ color: var(--muted);
+ font-size: 0.85rem;
+}
+
+.nav-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.nav-label {
+ margin: 12px 10px 6px;
+ color: var(--muted-2);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 12px 14px;
+ border-radius: 14px;
+ color: var(--muted);
+ transition:
+ transform 160ms ease,
+ background 160ms ease,
+ color 160ms ease;
+}
+
+.nav-link:hover {
+ transform: translateX(2px);
+ background: rgba(15, 118, 110, 0.08);
+ color: var(--text);
+}
+
+.nav-link.active {
+ color: white;
+ background: linear-gradient(135deg, var(--brand), #0f9488);
+ box-shadow: 0 12px 24px rgba(15, 118, 110, 0.24);
+}
+
+.nav-link small {
+ color: inherit;
+ opacity: 0.72;
+}
+
+.sidebar-footer {
+ margin-top: auto;
+ padding: 16px;
+ border-radius: 18px;
+ background: linear-gradient(135deg, rgba(255, 247, 237, 0.95), rgba(236, 253, 245, 0.9));
+ border: 1px solid rgba(217, 119, 6, 0.12);
+}
+
+.sidebar-footer strong {
+ display: block;
+ margin-bottom: 6px;
+}
+
+.sidebar-footer p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.92rem;
+ line-height: 1.5;
+}
+
+.content {
+ min-width: 0;
+ padding: 24px;
+}
+
+.content-inner {
+ max-width: var(--content-max);
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 18px 20px;
+ border-radius: var(--radius-xl);
+ background: var(--surface);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ box-shadow: var(--shadow-md);
+}
+
+.topbar-main {
+ min-width: 0;
+ flex: 1;
+}
+
+.eyebrow {
+ color: var(--brand);
+ text-transform: uppercase;
+ letter-spacing: 0.16em;
+ font-size: 0.74rem;
+ font-weight: 700;
+ margin-bottom: 4px;
+}
+
+.page-title {
+ margin: 0;
+ font-size: clamp(1.35rem, 2vw, 2rem);
+ line-height: 1.15;
+}
+
+.page-subtitle {
+ margin: 6px 0 0;
+ color: var(--muted);
+ line-height: 1.5;
+}
+
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.search {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 14px;
+ min-width: 280px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.92);
+ border: 1px solid var(--line);
+}
+
+.search input {
+ width: 100%;
+ border: 0;
+ outline: none;
+ background: transparent;
+ color: var(--text);
+}
+
+.icon-btn,
+.btn {
+ border: 0;
+ border-radius: 14px;
+ transition:
+ transform 160ms ease,
+ box-shadow 160ms ease,
+ background 160ms ease;
+}
+
+.icon-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 44px;
+ height: 44px;
+ background: rgba(255, 255, 255, 0.92);
+ border: 1px solid var(--line);
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ font-weight: 700;
+}
+
+.btn:hover,
+.icon-btn:hover {
+ transform: translateY(-1px);
+}
+
+.btn-primary {
+ color: white;
+ background: linear-gradient(135deg, var(--brand), #0f9488);
+ box-shadow: 0 14px 26px rgba(15, 118, 110, 0.22);
+}
+
+.btn-secondary {
+ color: var(--text);
+ background: white;
+ border: 1px solid var(--line);
+}
+
+.btn-ghost {
+ color: var(--muted);
+ background: rgba(255, 255, 255, 0.78);
+ border: 1px solid transparent;
+}
+
+.panel {
+ padding: 20px;
+ border-radius: var(--radius-xl);
+ background: var(--surface);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.72);
+ box-shadow: var(--shadow-md);
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.panel-header h2,
+.panel-header h3 {
+ margin: 0;
+}
+
+.panel-header p {
+ margin: 6px 0 0;
+ color: var(--muted);
+}
+
+.grid {
+ display: grid;
+ gap: 18px;
+}
+
+.grid-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.grid-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.grid-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.card {
+ padding: 18px;
+ border-radius: 22px;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1px solid rgba(15, 23, 42, 0.06);
+}
+
+.metric {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-height: 150px;
+}
+
+.metric .kicker {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.metric strong {
+ font-size: clamp(1.8rem, 2.2vw, 2.65rem);
+ letter-spacing: -0.04em;
+}
+
+.metric .trend {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--success);
+ font-size: 0.88rem;
+ font-weight: 700;
+}
+
+.pill-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 13px;
+ border-radius: 999px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 255, 255, 0.88);
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.pill.active {
+ color: var(--brand-strong);
+ background: rgba(15, 118, 110, 0.08);
+ border-color: rgba(15, 118, 110, 0.18);
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.table-wrap {
+ overflow: auto;
+ border-radius: 20px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ min-width: 860px;
+ background: rgba(255, 255, 255, 0.88);
+}
+
+th,
+td {
+ padding: 14px 16px;
+ border-bottom: 1px solid rgba(15, 23, 42, 0.06);
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ color: var(--muted);
+ font-size: 0.82rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ background: rgba(248, 250, 252, 0.98);
+}
+
+tr:last-child td {
+ border-bottom: 0;
+}
+
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 11px;
+ border-radius: 999px;
+ font-size: 0.84rem;
+ font-weight: 700;
+}
+
+.status::before {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: currentColor;
+}
+
+.status.success {
+ color: var(--success);
+ background: rgba(21, 128, 61, 0.08);
+}
+
+.status.warning {
+ color: var(--warning);
+ background: rgba(180, 83, 9, 0.09);
+}
+
+.status.danger {
+ color: var(--danger);
+ background: rgba(185, 28, 28, 0.09);
+}
+
+.status.neutral {
+ color: var(--muted);
+ background: rgba(15, 23, 42, 0.06);
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.75fr);
+ gap: 20px;
+}
+
+.mini-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.mini-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 14px;
+ padding: 14px 15px;
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.84);
+ border: 1px solid rgba(15, 23, 42, 0.06);
+}
+
+.mini-item strong {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.mini-item p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.stack {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.form {
+ display: grid;
+ gap: 16px;
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+}
+
+.field label {
+ font-weight: 700;
+ font-size: 0.92rem;
+}
+
+.field input,
+.field select,
+.field textarea {
+ width: 100%;
+ padding: 13px 14px;
+ border-radius: 14px;
+ border: 1px solid rgba(15, 23, 42, 0.12);
+ background: rgba(255, 255, 255, 0.9);
+ color: var(--text);
+ outline: none;
+}
+
+.field textarea {
+ min-height: 120px;
+ resize: vertical;
+}
+
+.field small {
+ color: var(--muted);
+}
+
+.field-inline {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.stepper {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.step {
+ flex: 1 1 160px;
+ padding: 14px;
+ border-radius: 16px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 255, 255, 0.82);
+}
+
+.step.active {
+ background: rgba(15, 118, 110, 0.08);
+ border-color: rgba(15, 118, 110, 0.18);
+}
+
+.step strong {
+ display: block;
+}
+
+.step span {
+ display: block;
+ margin-top: 4px;
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.note-box {
+ padding: 16px 18px;
+ border-radius: 18px;
+ background: linear-gradient(135deg, rgba(255, 247, 237, 0.85), rgba(240, 253, 250, 0.9));
+ border: 1px solid rgba(217, 119, 6, 0.12);
+ color: var(--text);
+}
+
+.auth-screen {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+}
+
+.auth-card {
+ width: min(1080px, 100%);
+ display: grid;
+ grid-template-columns: 1.1fr 0.9fr;
+ border-radius: 34px;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.84);
+ border: 1px solid rgba(255, 255, 255, 0.68);
+ box-shadow: var(--shadow-lg);
+}
+
+.auth-visual {
+ padding: 32px;
+ color: white;
+ background:
+ radial-gradient(circle at top right, rgba(255, 255, 255, 0.16), transparent 30%),
+ linear-gradient(135deg, #0f766e, #0f9488 52%, #115e59);
+}
+
+.auth-visual .badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.16);
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ margin-bottom: 24px;
+ font-size: 0.9rem;
+}
+
+.auth-visual h1 {
+ margin: 0;
+ font-size: clamp(2rem, 5vw, 4rem);
+ line-height: 1;
+}
+
+.auth-visual p {
+ max-width: 42ch;
+ font-size: 1.02rem;
+ line-height: 1.7;
+ opacity: 0.92;
+}
+
+.auth-points {
+ margin-top: 32px;
+ display: grid;
+ gap: 14px;
+}
+
+.auth-point {
+ padding: 14px 16px;
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.auth-form {
+ padding: 32px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 18px;
+}
+
+.auth-form h2 {
+ margin: 0;
+ font-size: 1.9rem;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.empty-state {
+ padding: 30px;
+ text-align: center;
+ border-radius: 24px;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1px dashed rgba(15, 23, 42, 0.14);
+}
+
+.empty-state p {
+ color: var(--muted);
+ margin: 8px 0 0;
+}
+
+.mobile-nav {
+ display: none;
+ position: sticky;
+ bottom: 0;
+ z-index: 10;
+ padding: 12px 14px 18px;
+ background: linear-gradient(180deg, rgba(244, 245, 247, 0.08), rgba(244, 245, 247, 0.96) 28%);
+}
+
+.mobile-nav-inner {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 8px;
+ padding: 10px;
+ border-radius: 22px;
+ background: rgba(255, 255, 255, 0.82);
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ box-shadow: var(--shadow-md);
+ backdrop-filter: blur(22px);
+}
+
+.mobile-nav .nav-link {
+ flex-direction: column;
+ justify-content: center;
+ gap: 4px;
+ padding: 11px 8px;
+ font-size: 0.78rem;
+ text-align: center;
+}
+
+.mobile-nav .nav-link small {
+ display: none;
+}
+
+.shell-footer {
+ display: none;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.desktop-only {
+ display: block;
+}
+
+.mobile-only {
+ display: none;
+}
+
+@media (max-width: 1180px) {
+ .grid-4,
+ .grid-3 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .detail-grid,
+ .auth-card {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 860px) {
+ .page-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ display: none;
+ }
+
+ .content {
+ padding: 16px 14px 0;
+ }
+
+ .topbar {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .topbar-actions {
+ justify-content: space-between;
+ }
+
+ .search {
+ min-width: 0;
+ }
+
+ .mobile-nav {
+ display: block;
+ }
+
+ .grid-4,
+ .grid-3,
+ .grid-2,
+ .field-inline {
+ grid-template-columns: 1fr;
+ }
+
+ .desktop-only {
+ display: none;
+ }
+
+ .mobile-only {
+ display: grid;
+ gap: 12px;
+ }
+
+ .panel,
+ .topbar {
+ border-radius: 22px;
+ }
+
+ .auth-screen {
+ padding: 12px;
+ }
+}
+
+@media (max-width: 620px) {
+ .content {
+ padding: 12px 10px 0;
+ }
+
+ .content-inner {
+ gap: 14px;
+ }
+
+ .panel,
+ .topbar,
+ .auth-form,
+ .auth-visual {
+ padding: 16px;
+ }
+
+ .mobile-nav-inner {
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 4px;
+ padding: 8px;
+ }
+
+ .btn {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .topbar-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .btn,
+ .icon-btn,
+ .search {
+ width: 100%;
+ }
+
+ .table-wrap {
+ border-radius: 18px;
+ }
+}