Spaces:
Build error
Build error
Rick
commited on
Commit
·
8ed4e01
1
Parent(s):
e45be98
Add triage samples and update medical resources UI
Browse files- app.py +117 -25
- static/data/triage_samples.json +602 -0
- static/js/chat.js +249 -16
- static/js/crew.js +251 -17
- static/js/equipment.js +12 -0
- static/js/main.js +11 -3
- static/js/pharmacy.js +73 -11
- static/js/settings.js +81 -2
- templates/index.html +154 -25
- templates/sidebars/sidebar_chat.html +1 -1
- templates/sidebars/sidebar_crew_medical.html +12 -1
app.py
CHANGED
|
@@ -324,6 +324,7 @@ def get_defaults():
|
|
| 324 |
"generic_name, brand_name, form, strength, expiry_date, batch_lot, "
|
| 325 |
"storage_location, manufacturer, indication, allergy_warnings, dosage, notes."
|
| 326 |
),
|
|
|
|
| 327 |
}
|
| 328 |
|
| 329 |
|
|
@@ -347,6 +348,14 @@ def db_op(cat, data=None, workspace=None):
|
|
| 347 |
"homePort": "",
|
| 348 |
"callSign": "",
|
| 349 |
"tonnage": "",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
"crewCapacity": "",
|
| 351 |
}
|
| 352 |
else:
|
|
@@ -369,7 +378,12 @@ def db_op(cat, data=None, workspace=None):
|
|
| 369 |
path.write_text(json.dumps(data, indent=4))
|
| 370 |
return data
|
| 371 |
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
def safe_float(val, default):
|
|
@@ -393,6 +407,40 @@ def _is_resource_excluded(item):
|
|
| 393 |
return bool(val)
|
| 394 |
|
| 395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
def build_prompt(settings, mode, msg, p_name, workspace):
|
| 397 |
rep_penalty = safe_float(settings.get("rep_penalty", 1.1) or 1.1, 1.1)
|
| 398 |
mission_context = settings.get("mission_context", "")
|
|
@@ -421,19 +469,19 @@ def build_prompt(settings, mode, msg, p_name, workspace):
|
|
| 421 |
continue
|
| 422 |
if not item_name:
|
| 423 |
continue
|
| 424 |
-
cat = (m.get("type") or "").strip().lower()
|
| 425 |
key = (item_name or "").strip().lower()
|
| 426 |
if not key:
|
| 427 |
continue
|
| 428 |
-
if cat
|
| 429 |
pharma_items[key] = item_name
|
| 430 |
elif cat == "consumable":
|
| 431 |
consumable_items[key] = item_name
|
| 432 |
elif cat == "equipment":
|
| 433 |
equip_items[key] = item_name
|
| 434 |
else:
|
| 435 |
-
#
|
| 436 |
-
|
| 437 |
pharma_list = [pharma_items[k] for k in sorted(pharma_items)]
|
| 438 |
equip_list = [equip_items[k] for k in sorted(equip_items)]
|
| 439 |
consumable_list = [consumable_items[k] for k in sorted(consumable_items)]
|
|
@@ -447,21 +495,7 @@ def build_prompt(settings, mode, msg, p_name, workspace):
|
|
| 447 |
if tool_name:
|
| 448 |
tool_items.append(tool_name)
|
| 449 |
tool_items.sort(key=lambda s: (s or "").lower())
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
def _patient_display_name(record, fallback):
|
| 453 |
-
if not record:
|
| 454 |
-
return fallback
|
| 455 |
-
name = record.get("name") or record.get("fullName") or ""
|
| 456 |
-
if name and name.strip():
|
| 457 |
-
return name
|
| 458 |
-
parts = [
|
| 459 |
-
record.get("firstName") or "",
|
| 460 |
-
record.get("middleName") or "",
|
| 461 |
-
record.get("lastName") or "",
|
| 462 |
-
]
|
| 463 |
-
combined = " ".join(part for part in parts if part).strip()
|
| 464 |
-
return combined or fallback
|
| 465 |
|
| 466 |
patient_record = next(
|
| 467 |
(
|
|
@@ -475,20 +509,67 @@ def build_prompt(settings, mode, msg, p_name, workspace):
|
|
| 475 |
p_hist = patient_record.get("history", "No records.")
|
| 476 |
p_sex = patient_record.get("sex") or patient_record.get("gender") or "Unknown"
|
| 477 |
p_birth = patient_record.get("birthdate") or "Unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
prompt_sections = [
|
| 480 |
f"MISSION CONTEXT: {mission_context}" if mission_context else "",
|
| 481 |
f"TRIAGE INSTRUCTION:\n{settings.get('triage_instruction')}",
|
| 482 |
"RESOURCES:\n"
|
| 483 |
f"- Pharmaceuticals: {pharma_str or 'None listed'}\n"
|
| 484 |
-
f"- Medical Equipment: {equip_str or 'None listed'}\n"
|
| 485 |
-
f"- Consumables: {consumable_str or 'None listed'}
|
| 486 |
-
f"- Tools: {tools or 'None listed'}",
|
| 487 |
"PATIENT:\n"
|
| 488 |
f"- Name: {display_name}\n"
|
| 489 |
f"- Sex: {p_sex}\n"
|
| 490 |
f"- Date of Birth: {p_birth}\n"
|
| 491 |
-
f"- Medical History (profile): {p_hist or 'No records.'}"
|
|
|
|
| 492 |
f"SITUATION:\n{msg}",
|
| 493 |
]
|
| 494 |
prompt = "\n\n".join(section for section in prompt_sections if section.strip())
|
|
@@ -1394,6 +1475,7 @@ async def chat(request: Request, _=Depends(require_auth)):
|
|
| 1394 |
start_time = datetime.now()
|
| 1395 |
form = await request.form()
|
| 1396 |
msg = form.get("message")
|
|
|
|
| 1397 |
p_name = form.get("patient")
|
| 1398 |
mode = form.get("mode")
|
| 1399 |
is_priv = form.get("private") == "true"
|
|
@@ -1454,15 +1536,25 @@ async def chat(request: Request, _=Depends(require_auth)):
|
|
| 1454 |
|
| 1455 |
if not is_priv:
|
| 1456 |
h = db_op("history", workspace=workspace)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1457 |
h.append(
|
| 1458 |
{
|
| 1459 |
"id": datetime.now().isoformat(),
|
| 1460 |
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 1461 |
-
"patient":
|
|
|
|
|
|
|
| 1462 |
"query": msg,
|
|
|
|
| 1463 |
"response": res,
|
| 1464 |
"model": models["active_name"],
|
| 1465 |
"duration_ms": elapsed_ms,
|
|
|
|
|
|
|
| 1466 |
}
|
| 1467 |
)
|
| 1468 |
db_op("history", h, workspace=workspace)
|
|
|
|
| 324 |
"generic_name, brand_name, form, strength, expiry_date, batch_lot, "
|
| 325 |
"storage_location, manufacturer, indication, allergy_warnings, dosage, notes."
|
| 326 |
),
|
| 327 |
+
"vaccine_types": ["MMR", "DTaP", "HepB", "HepA", "Td/Tdap", "Influenza", "COVID-19"],
|
| 328 |
}
|
| 329 |
|
| 330 |
|
|
|
|
| 348 |
"homePort": "",
|
| 349 |
"callSign": "",
|
| 350 |
"tonnage": "",
|
| 351 |
+
"netTonnage": "",
|
| 352 |
+
"mmsi": "",
|
| 353 |
+
"hullNumber": "",
|
| 354 |
+
"starboardEngine": "",
|
| 355 |
+
"starboardEngineSn": "",
|
| 356 |
+
"portEngine": "",
|
| 357 |
+
"portEngineSn": "",
|
| 358 |
+
"ribSn": "",
|
| 359 |
"crewCapacity": "",
|
| 360 |
}
|
| 361 |
else:
|
|
|
|
| 378 |
path.write_text(json.dumps(data, indent=4))
|
| 379 |
return data
|
| 380 |
|
| 381 |
+
loaded = json.loads(path.read_text() or "[]")
|
| 382 |
+
if cat == "settings":
|
| 383 |
+
if not isinstance(loaded, dict):
|
| 384 |
+
loaded = {}
|
| 385 |
+
return {**get_defaults(), **loaded}
|
| 386 |
+
return loaded
|
| 387 |
|
| 388 |
|
| 389 |
def safe_float(val, default):
|
|
|
|
| 407 |
return bool(val)
|
| 408 |
|
| 409 |
|
| 410 |
+
def _patient_display_name(record, fallback):
|
| 411 |
+
if not record:
|
| 412 |
+
return fallback
|
| 413 |
+
name = record.get("name") or record.get("fullName") or ""
|
| 414 |
+
if name and name.strip():
|
| 415 |
+
return name
|
| 416 |
+
parts = [
|
| 417 |
+
record.get("firstName") or "",
|
| 418 |
+
record.get("middleName") or "",
|
| 419 |
+
record.get("lastName") or "",
|
| 420 |
+
]
|
| 421 |
+
combined = " ".join(part for part in parts if part).strip()
|
| 422 |
+
return combined or fallback
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def lookup_patient_display_name(p_name, workspace, default="Unnamed Crew"):
|
| 426 |
+
if not p_name:
|
| 427 |
+
return default
|
| 428 |
+
try:
|
| 429 |
+
patients = db_op("patients", workspace=workspace)
|
| 430 |
+
except Exception:
|
| 431 |
+
return default
|
| 432 |
+
rec = next(
|
| 433 |
+
(
|
| 434 |
+
p
|
| 435 |
+
for p in patients
|
| 436 |
+
if (p.get("id") and p.get("id") == p_name)
|
| 437 |
+
or (p.get("name") and p.get("name") == p_name)
|
| 438 |
+
),
|
| 439 |
+
None,
|
| 440 |
+
)
|
| 441 |
+
return _patient_display_name(rec, p_name or default)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
def build_prompt(settings, mode, msg, p_name, workspace):
|
| 445 |
rep_penalty = safe_float(settings.get("rep_penalty", 1.1) or 1.1, 1.1)
|
| 446 |
mission_context = settings.get("mission_context", "")
|
|
|
|
| 469 |
continue
|
| 470 |
if not item_name:
|
| 471 |
continue
|
| 472 |
+
cat = (m.get("type") or "medication").strip().lower()
|
| 473 |
key = (item_name or "").strip().lower()
|
| 474 |
if not key:
|
| 475 |
continue
|
| 476 |
+
if cat in {"medication", ""}:
|
| 477 |
pharma_items[key] = item_name
|
| 478 |
elif cat == "consumable":
|
| 479 |
consumable_items[key] = item_name
|
| 480 |
elif cat == "equipment":
|
| 481 |
equip_items[key] = item_name
|
| 482 |
else:
|
| 483 |
+
# Default unknown types to medication so they are not dropped
|
| 484 |
+
pharma_items[key] = item_name
|
| 485 |
pharma_list = [pharma_items[k] for k in sorted(pharma_items)]
|
| 486 |
equip_list = [equip_items[k] for k in sorted(equip_items)]
|
| 487 |
consumable_list = [consumable_items[k] for k in sorted(consumable_items)]
|
|
|
|
| 495 |
if tool_name:
|
| 496 |
tool_items.append(tool_name)
|
| 497 |
tool_items.sort(key=lambda s: (s or "").lower())
|
| 498 |
+
equipment_extra = ", ".join(tool_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
patient_record = next(
|
| 501 |
(
|
|
|
|
| 509 |
p_hist = patient_record.get("history", "No records.")
|
| 510 |
p_sex = patient_record.get("sex") or patient_record.get("gender") or "Unknown"
|
| 511 |
p_birth = patient_record.get("birthdate") or "Unknown"
|
| 512 |
+
vaccines = patient_record.get("vaccines") or []
|
| 513 |
+
|
| 514 |
+
def _format_vaccines(vax_list):
|
| 515 |
+
if not isinstance(vax_list, list) or not vax_list:
|
| 516 |
+
return "No vaccines recorded."
|
| 517 |
+
formatted = []
|
| 518 |
+
for v in vax_list:
|
| 519 |
+
if not isinstance(v, dict):
|
| 520 |
+
continue
|
| 521 |
+
parts = []
|
| 522 |
+
v_type = v.get("vaccineType") or "Vaccine"
|
| 523 |
+
date = v.get("dateAdministered")
|
| 524 |
+
dose = v.get("doseNumber")
|
| 525 |
+
trade = v.get("tradeNameManufacturer")
|
| 526 |
+
lot = v.get("lotNumber")
|
| 527 |
+
provider = v.get("provider")
|
| 528 |
+
provider_country = v.get("providerCountry")
|
| 529 |
+
next_due = v.get("nextDoseDue")
|
| 530 |
+
exp = v.get("expirationDate")
|
| 531 |
+
site = v.get("siteRoute")
|
| 532 |
+
reactions = v.get("reactions")
|
| 533 |
+
if date:
|
| 534 |
+
parts.append(f"Date: {date}")
|
| 535 |
+
if dose:
|
| 536 |
+
parts.append(f"Dose: {dose}")
|
| 537 |
+
if trade:
|
| 538 |
+
parts.append(f"Trade/Manufacturer: {trade}")
|
| 539 |
+
if lot:
|
| 540 |
+
parts.append(f"Lot: {lot}")
|
| 541 |
+
if provider:
|
| 542 |
+
parts.append(f"Provider: {provider}")
|
| 543 |
+
if provider_country:
|
| 544 |
+
parts.append(f"Provider Country: {provider_country}")
|
| 545 |
+
if next_due:
|
| 546 |
+
parts.append(f"Next Dose Due: {next_due}")
|
| 547 |
+
if exp:
|
| 548 |
+
parts.append(f"Expiration: {exp}")
|
| 549 |
+
if site:
|
| 550 |
+
parts.append(f"Site/Route: {site}")
|
| 551 |
+
if reactions:
|
| 552 |
+
parts.append(f"Reactions: {reactions}")
|
| 553 |
+
details = "; ".join(parts)
|
| 554 |
+
if details:
|
| 555 |
+
formatted.append(f"{v_type} ({details})")
|
| 556 |
+
else:
|
| 557 |
+
formatted.append(v_type)
|
| 558 |
+
return "; ".join(formatted) if formatted else "No vaccines recorded."
|
| 559 |
|
| 560 |
prompt_sections = [
|
| 561 |
f"MISSION CONTEXT: {mission_context}" if mission_context else "",
|
| 562 |
f"TRIAGE INSTRUCTION:\n{settings.get('triage_instruction')}",
|
| 563 |
"RESOURCES:\n"
|
| 564 |
f"- Pharmaceuticals: {pharma_str or 'None listed'}\n"
|
| 565 |
+
f"- Medical Equipment: {equip_str or equipment_extra or 'None listed'}\n"
|
| 566 |
+
f"- Consumables: {consumable_str or 'None listed'}",
|
|
|
|
| 567 |
"PATIENT:\n"
|
| 568 |
f"- Name: {display_name}\n"
|
| 569 |
f"- Sex: {p_sex}\n"
|
| 570 |
f"- Date of Birth: {p_birth}\n"
|
| 571 |
+
f"- Medical History (profile): {p_hist or 'No records.'}\n"
|
| 572 |
+
f"- Vaccines: {_format_vaccines(vaccines)}",
|
| 573 |
f"SITUATION:\n{msg}",
|
| 574 |
]
|
| 575 |
prompt = "\n\n".join(section for section in prompt_sections if section.strip())
|
|
|
|
| 1475 |
start_time = datetime.now()
|
| 1476 |
form = await request.form()
|
| 1477 |
msg = form.get("message")
|
| 1478 |
+
user_msg_raw = msg
|
| 1479 |
p_name = form.get("patient")
|
| 1480 |
mode = form.get("mode")
|
| 1481 |
is_priv = form.get("private") == "true"
|
|
|
|
| 1536 |
|
| 1537 |
if not is_priv:
|
| 1538 |
h = db_op("history", workspace=workspace)
|
| 1539 |
+
patient_display = (
|
| 1540 |
+
lookup_patient_display_name(p_name, workspace, default="Unnamed Crew")
|
| 1541 |
+
if mode == "triage"
|
| 1542 |
+
else "Inquiry"
|
| 1543 |
+
)
|
| 1544 |
h.append(
|
| 1545 |
{
|
| 1546 |
"id": datetime.now().isoformat(),
|
| 1547 |
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 1548 |
+
"patient": patient_display,
|
| 1549 |
+
"patient_id": p_name or "",
|
| 1550 |
+
"mode": mode,
|
| 1551 |
"query": msg,
|
| 1552 |
+
"user_query": user_msg_raw,
|
| 1553 |
"response": res,
|
| 1554 |
"model": models["active_name"],
|
| 1555 |
"duration_ms": elapsed_ms,
|
| 1556 |
+
"prompt": prompt,
|
| 1557 |
+
"injected_prompt": prompt,
|
| 1558 |
}
|
| 1559 |
)
|
| 1560 |
db_op("history", h, workspace=workspace)
|
static/data/triage_samples.json
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": 1,
|
| 4 |
+
"situation": "Open Femur Fracture",
|
| 5 |
+
"chat_text": "One of the crew fell from the mast and their thigh is completely deformed with a bone sticking out. They are barely awake and breathing very fast. There is a lot of blood and they look incredibly pale.",
|
| 6 |
+
"responsive": "Drowsy",
|
| 7 |
+
"breathing": "Rapid/Shallow",
|
| 8 |
+
"pain": "10/10",
|
| 9 |
+
"main_problem": "Heavy bleeding/deformed thigh",
|
| 10 |
+
"temp": "36.2°C",
|
| 11 |
+
"circulation": "Pale, weak pulse, BP 90/60",
|
| 12 |
+
"cause": "Fall from mast during squall"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"id": 2,
|
| 16 |
+
"situation": "Tension Pneumothorax",
|
| 17 |
+
"chat_text": "He got hit hard in the chest by the boom and now he can't catch his breath. He’s struggling to breathe, his neck veins are bulging out, and his lips are turning blue.",
|
| 18 |
+
"responsive": "Alert/Anxious",
|
| 19 |
+
"breathing": "Struggling",
|
| 20 |
+
"pain": "8/10",
|
| 21 |
+
"main_problem": "One side of chest not moving",
|
| 22 |
+
"temp": "37.0°C",
|
| 23 |
+
"circulation": "Distended neck veins, low BP",
|
| 24 |
+
"cause": "Blown into shroud by boom"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": 3,
|
| 28 |
+
"situation": "Severe Scalp Laceration",
|
| 29 |
+
"chat_text": "A heavy block hit her in the head and blood is literally pulsing out in a spray. She’s awake but there’s a massive amount of bright red blood everywhere.",
|
| 30 |
+
"responsive": "Alert",
|
| 31 |
+
"breathing": "Normal",
|
| 32 |
+
"pain": "7/10",
|
| 33 |
+
"main_problem": "Arterial spurting from head",
|
| 34 |
+
"temp": "36.8°C",
|
| 35 |
+
"circulation": "Rapid pulse, BP normal",
|
| 36 |
+
"cause": "Hit by mainsheet block"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"id": 4,
|
| 40 |
+
"situation": "Traumatic Amputation",
|
| 41 |
+
"chat_text": "Her hand got sucked into the electric winch. Three fingers are gone and the stumps are bleeding uncontrollably. She’s passed out and her skin is cold and white.",
|
| 42 |
+
"responsive": "Unconscious",
|
| 43 |
+
"breathing": "Labored",
|
| 44 |
+
"pain": "N/A",
|
| 45 |
+
"main_problem": "Missing fingers (R hand)",
|
| 46 |
+
"temp": "35.5°C",
|
| 47 |
+
"circulation": "Massive hemorrhage, shock",
|
| 48 |
+
"cause": "Hand caught in electric winch"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": 5,
|
| 52 |
+
"situation": "Internal Hemorrhage",
|
| 53 |
+
"chat_text": "He was thrown against the cockpit table during a roll. His stomach is becoming very hard and bloated, he’s breathing fast, and he looks like he’s going into shock.",
|
| 54 |
+
"responsive": "Drowsy",
|
| 55 |
+
"breathing": "Fast",
|
| 56 |
+
"pain": "6/10",
|
| 57 |
+
"main_problem": "Distended/rigid abdomen",
|
| 58 |
+
"temp": "36.0°C",
|
| 59 |
+
"circulation": "Cold/clammy, BP dropping",
|
| 60 |
+
"cause": "Thrown against cockpit table"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": 6,
|
| 64 |
+
"situation": "Crush Injury (Foot)",
|
| 65 |
+
"chat_text": "An anchor fell on his foot. The pain is a 9 out of 10, his foot is turning blue and cold, and I can't find a pulse anywhere on his ankle.",
|
| 66 |
+
"responsive": "Normal",
|
| 67 |
+
"breathing": "Normal",
|
| 68 |
+
"pain": "9/10",
|
| 69 |
+
"main_problem": "Swollen, blue, no pulse in foot",
|
| 70 |
+
"temp": "37.2°C",
|
| 71 |
+
"circulation": "Good BP, peripheral blockage",
|
| 72 |
+
"cause": "Heavy anchor dropped on foot"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": 7,
|
| 76 |
+
"situation": "Flail Chest",
|
| 77 |
+
"chat_text": "He slammed his chest into a winch. Part of his ribcage is moving inward when he breathes in and outward when he breathes out. He’s in a lot of pain and struggling for air.",
|
| 78 |
+
"responsive": "Alert",
|
| 79 |
+
"breathing": "Very painful",
|
| 80 |
+
"pain": "9/10",
|
| 81 |
+
"main_problem": "Paradoxical chest movement",
|
| 82 |
+
"temp": "37.1°C",
|
| 83 |
+
"circulation": "Fast pulse",
|
| 84 |
+
"cause": "Chest slammed into winch"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"id": 8,
|
| 88 |
+
"situation": "Concussion/TBI",
|
| 89 |
+
"chat_text": "She hit her head on the deck. She’s very confused, keep throwing up, and one of her pupils is much larger than the other.",
|
| 90 |
+
"responsive": "Confused",
|
| 91 |
+
"breathing": "Normal",
|
| 92 |
+
"pain": "5/10",
|
| 93 |
+
"main_problem": "Repeated vomiting, pupil dilation",
|
| 94 |
+
"temp": "36.9°C",
|
| 95 |
+
"circulation": "BP 140/90 (Rising)",
|
| 96 |
+
"cause": "Slip on wet deck, head hit GRP"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": 9,
|
| 100 |
+
"situation": "Dislocated Shoulder",
|
| 101 |
+
"chat_text": "He reached for the rail during a big wave and his shoulder popped out. It looks completely misshapen and he can't move his arm at all.",
|
| 102 |
+
"responsive": "Alert",
|
| 103 |
+
"breathing": "Normal",
|
| 104 |
+
"pain": "8/10",
|
| 105 |
+
"main_problem": "Visual deformity, arm locked",
|
| 106 |
+
"temp": "37.0°C",
|
| 107 |
+
"circulation": "Normal",
|
| 108 |
+
"cause": "Reaching for rail during roll"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": 10,
|
| 112 |
+
"situation": "Impaled Object",
|
| 113 |
+
"chat_text": "The spinnaker pole shattered and a long, sharp piece of carbon fiber is stuck deep in his thigh. It’s not bleeding much but the object is still in there.",
|
| 114 |
+
"responsive": "Alert",
|
| 115 |
+
"breathing": "Normal",
|
| 116 |
+
"pain": "7/10",
|
| 117 |
+
"main_problem": "Shard of carbon fiber in thigh",
|
| 118 |
+
"temp": "36.8°C",
|
| 119 |
+
"circulation": "Steady, bleeding controlled",
|
| 120 |
+
"cause": "Shattered spinnaker pole"
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"id": 11,
|
| 124 |
+
"situation": "Severe Hypothermia",
|
| 125 |
+
"chat_text": "We pulled him out of the water after 30 minutes. He’s stopped shivering, he’s mumbles when he speaks, and his body feels ice cold and stiff.",
|
| 126 |
+
"responsive": "Mumbling",
|
| 127 |
+
"breathing": "Very slow",
|
| 128 |
+
"pain": "None",
|
| 129 |
+
"main_problem": "Shivering stopped, rigid",
|
| 130 |
+
"temp": "31.0°C",
|
| 131 |
+
"circulation": "Barely palpable pulse",
|
| 132 |
+
"cause": "30 mins in 15°C water (MOB)"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"id": 12,
|
| 136 |
+
"situation": "Heat Stroke",
|
| 137 |
+
"chat_text": "He’s been working in the engine room and now he’s unconscious. His skin is red and bone dry, he’s having a seizure, and he feels like he's burning up.",
|
| 138 |
+
"responsive": "Unconscious",
|
| 139 |
+
"breathing": "Snoring",
|
| 140 |
+
"pain": "N/A",
|
| 141 |
+
"main_problem": "Hot, dry skin; seizures",
|
| 142 |
+
"temp": "41.1°C",
|
| 143 |
+
"circulation": "Tachycardia (140 bpm)",
|
| 144 |
+
"cause": "Engine room repair in tropics"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"id": 13,
|
| 148 |
+
"situation": "Saltwater Aspiration",
|
| 149 |
+
"chat_text": "She swallowed a lot of water when she fell overboard. She’s coughing constantly, gasping for air, and her lips look blue.",
|
| 150 |
+
"responsive": "Alert",
|
| 151 |
+
"breathing": "Gasping",
|
| 152 |
+
"pain": "6/10",
|
| 153 |
+
"main_problem": "Persistent coughing, blue lips",
|
| 154 |
+
"temp": "37.5°C",
|
| 155 |
+
"circulation": "Rapid pulse",
|
| 156 |
+
"cause": "Swallowed water during MOB"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"id": 14,
|
| 160 |
+
"situation": "Severe Dehydration",
|
| 161 |
+
"chat_text": "He’s been seasick for days and hasn't peed in 24 hours. His eyes are sunken in, he’s very weak, and he has a slight fever.",
|
| 162 |
+
"responsive": "Lethargic",
|
| 163 |
+
"breathing": "Normal",
|
| 164 |
+
"pain": "4/10",
|
| 165 |
+
"main_problem": "No urine for 24h, sunken eyes",
|
| 166 |
+
"temp": "38.2°C",
|
| 167 |
+
"circulation": "Weak pulse, very low BP",
|
| 168 |
+
"cause": "Chronic seasickness/vomiting"
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": 15,
|
| 172 |
+
"situation": "2nd Degree Sunburn",
|
| 173 |
+
"chat_text": "He fell asleep on deck and has a massive sunburn. Almost half his body is covered in large blisters and he’s shaking even though he has a fever.",
|
| 174 |
+
"responsive": "Alert",
|
| 175 |
+
"breathing": "Normal",
|
| 176 |
+
"pain": "8/10",
|
| 177 |
+
"main_problem": "Blistering over 40% of body",
|
| 178 |
+
"temp": "38.5°C",
|
| 179 |
+
"circulation": "Shivers, mild hypotension",
|
| 180 |
+
"cause": "Fallen asleep on deck in doldrums"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": 16,
|
| 184 |
+
"situation": "Immersion Foot",
|
| 185 |
+
"chat_text": "His boots have been wet for four days straight. His feet are completely white, numb, and the skin is starting to peel off in chunks.",
|
| 186 |
+
"responsive": "Alert",
|
| 187 |
+
"breathing": "Normal",
|
| 188 |
+
"pain": "6/10",
|
| 189 |
+
"main_problem": "Feet white, numb, peeling",
|
| 190 |
+
"temp": "36.5°C",
|
| 191 |
+
"circulation": "Poor capillary refill",
|
| 192 |
+
"cause": "4 days in wet boots on watch"
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"id": 17,
|
| 196 |
+
"situation": "Severe Hyponatremia",
|
| 197 |
+
"chat_text": "He’s been drinking gallons of water but eating no salt. Now he’s staggering around like he’s drunk and his speech is totally slurred.",
|
| 198 |
+
"responsive": "Confused",
|
| 199 |
+
"breathing": "Normal",
|
| 200 |
+
"pain": "2/10",
|
| 201 |
+
"main_problem": "Slurred speech, staggering",
|
| 202 |
+
"temp": "37.0°C",
|
| 203 |
+
"circulation": "Normal BP",
|
| 204 |
+
"cause": "Over-drinking water, no salt"
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": 18,
|
| 208 |
+
"situation": "Deep Frostbite",
|
| 209 |
+
"chat_text": "We’ve been handling ice-cold lines and his fingers have turned hard and gray-black. He can't feel them at all.",
|
| 210 |
+
"responsive": "Alert",
|
| 211 |
+
"breathing": "Normal",
|
| 212 |
+
"pain": "3/10",
|
| 213 |
+
"main_problem": "Fingers hard, black/gray",
|
| 214 |
+
"temp": "35.8°C",
|
| 215 |
+
"circulation": "Poor circulation to hand",
|
| 216 |
+
"cause": "Hand handling icy lines"
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"id": 19,
|
| 220 |
+
"situation": "Nitrogen Narcosis",
|
| 221 |
+
"chat_text": "He came up too fast after checking the hull. He’s acting very strangely, giggling, and seems totally confused about where he is.",
|
| 222 |
+
"responsive": "Giggling",
|
| 223 |
+
"breathing": "Fast",
|
| 224 |
+
"pain": "None",
|
| 225 |
+
"main_problem": "Irrational behavior/confusion",
|
| 226 |
+
"temp": "36.7°C",
|
| 227 |
+
"circulation": "Normal",
|
| 228 |
+
"cause": "Rapid ascent from hull check"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": 20,
|
| 232 |
+
"situation": "Lightning Strike",
|
| 233 |
+
"chat_text": "The boat was hit by lightning. He’s unconscious and not breathing. I can't find a pulse and there are weird burn marks on his skin.",
|
| 234 |
+
"responsive": "Unconscious",
|
| 235 |
+
"breathing": "Arrested",
|
| 236 |
+
"pain": "N/A",
|
| 237 |
+
"main_problem": "Cardiac arrest, \"feather\" burns",
|
| 238 |
+
"temp": "36.4°C",
|
| 239 |
+
"circulation": "No pulse (requires CPR)",
|
| 240 |
+
"cause": "Direct hit on mast during storm"
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"id": 21,
|
| 244 |
+
"situation": "Acute Appendicitis",
|
| 245 |
+
"chat_text": "She has a 9 out of 10 pain in her lower right stomach. If I press down and let go, the pain is even worse. She also has a fever.",
|
| 246 |
+
"responsive": "Alert",
|
| 247 |
+
"breathing": "Guarded",
|
| 248 |
+
"pain": "9/10",
|
| 249 |
+
"main_problem": "Rebound tenderness lower R",
|
| 250 |
+
"temp": "38.9°C",
|
| 251 |
+
"circulation": "High BP from pain",
|
| 252 |
+
"cause": "Random/Bacterial"
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
"id": 22,
|
| 256 |
+
"situation": "Sepsis (Infected Wound)",
|
| 257 |
+
"chat_text": "An old coral cut on his leg has red streaks coming out of it. He’s shaking with chills, has a high fever, and his blood pressure seems very low.",
|
| 258 |
+
"responsive": "Drowsy",
|
| 259 |
+
"breathing": "Rapid",
|
| 260 |
+
"pain": "5/10",
|
| 261 |
+
"main_problem": "Red streaks, shaking chills",
|
| 262 |
+
"temp": "39.5°C",
|
| 263 |
+
"circulation": "BP 80/50 (Septic shock)",
|
| 264 |
+
"cause": "Uncleaned coral cut"
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"id": 23,
|
| 268 |
+
"situation": "Anaphylaxis",
|
| 269 |
+
"chat_text": "He got bit by an insect and now his throat is swelling up. He’s wheezing, covered in hives, and looks like he’s about to pass out.",
|
| 270 |
+
"responsive": "Drowsy",
|
| 271 |
+
"breathing": "Wheezing",
|
| 272 |
+
"pain": "4/10",
|
| 273 |
+
"main_problem": "Swollen throat, hives",
|
| 274 |
+
"temp": "37.2°C",
|
| 275 |
+
"circulation": "BP dropping rapidly",
|
| 276 |
+
"cause": "Unknown insect bite/food"
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"id": 24,
|
| 280 |
+
"situation": "Myocardial Infarction",
|
| 281 |
+
"chat_text": "He says it feels like an elephant is sitting on his chest. The pain is going down his left arm, he’s sweating, and his pulse feels irregular.",
|
| 282 |
+
"responsive": "Alert",
|
| 283 |
+
"breathing": "Shortness",
|
| 284 |
+
"pain": "9/10",
|
| 285 |
+
"main_problem": "Crushing chest pain/Left arm",
|
| 286 |
+
"temp": "37.0°C",
|
| 287 |
+
"circulation": "Irregular pulse, sweating",
|
| 288 |
+
"cause": "Clogged artery (Heart Attack)"
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"id": 25,
|
| 292 |
+
"situation": "Diabetic Ketoacidosis",
|
| 293 |
+
"chat_text": "He’s very confused and his breath smells sweet, almost like fruit. He’s breathing very deeply and fast and says he’s incredibly thirsty.",
|
| 294 |
+
"responsive": "Confused",
|
| 295 |
+
"breathing": "Deep/Fast",
|
| 296 |
+
"pain": "3/10",
|
| 297 |
+
"main_problem": "Fruity breath, extreme thirst",
|
| 298 |
+
"temp": "37.4°C",
|
| 299 |
+
"circulation": "Weak pulse",
|
| 300 |
+
"cause": "Insulin pump failure"
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"id": 26,
|
| 304 |
+
"situation": "Perforated Ulcer",
|
| 305 |
+
"chat_text": "He has a sudden, agonizing pain in his stomach. His belly feels as hard as a board and he’s pale and sweating.",
|
| 306 |
+
"responsive": "Alert",
|
| 307 |
+
"breathing": "Shallow",
|
| 308 |
+
"pain": "10/10",
|
| 309 |
+
"main_problem": "Sudden, board-like abdomen",
|
| 310 |
+
"temp": "37.8°C",
|
| 311 |
+
"circulation": "Shock signs",
|
| 312 |
+
"cause": "Long-term NSAID use (Advil)"
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": 27,
|
| 316 |
+
"situation": "Kidney Stones",
|
| 317 |
+
"chat_text": "He’s in 10 out of 10 pain in his side and back. He’s pacing around because he can't get comfortable and there is blood in his urine.",
|
| 318 |
+
"responsive": "Alert",
|
| 319 |
+
"breathing": "Fast",
|
| 320 |
+
"pain": "10/10",
|
| 321 |
+
"main_problem": "Agonizing flank pain/blood",
|
| 322 |
+
"temp": "37.3°C",
|
| 323 |
+
"circulation": "Pacing around, high BP",
|
| 324 |
+
"cause": "Dehydration"
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"id": 28,
|
| 328 |
+
"situation": "Acute Asthma Attack",
|
| 329 |
+
"chat_text": "She’s having a massive asthma attack. Her inhaler isn't working and I can't hear any air moving in her chest at all. Her lips are turning blue.",
|
| 330 |
+
"responsive": "Alert",
|
| 331 |
+
"breathing": "Silent",
|
| 332 |
+
"pain": "7/10",
|
| 333 |
+
"main_problem": "No air movement (Silent chest)",
|
| 334 |
+
"temp": "37.0°C",
|
| 335 |
+
"circulation": "Tachycardia",
|
| 336 |
+
"cause": "Mold in cabin/ventilation"
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"id": 29,
|
| 340 |
+
"situation": "Ischemic Stroke",
|
| 341 |
+
"chat_text": "One side of his face is drooping and he can't move his right arm or leg. He’s awake but his blood pressure is extremely high.",
|
| 342 |
+
"responsive": "Alert",
|
| 343 |
+
"breathing": "Normal",
|
| 344 |
+
"pain": "2/10",
|
| 345 |
+
"main_problem": "Facial droop, R-side paralysis",
|
| 346 |
+
"temp": "36.8°C",
|
| 347 |
+
"circulation": "BP 180/110",
|
| 348 |
+
"cause": "Blood clot"
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"id": 30,
|
| 352 |
+
"situation": "Status Epilepticus",
|
| 353 |
+
"chat_text": "He’s been having a violent seizure for over five minutes straight and it won't stop. His breathing is irregular and he’s turning red.",
|
| 354 |
+
"responsive": "Seizing",
|
| 355 |
+
"breathing": "Irregular",
|
| 356 |
+
"pain": "N/A",
|
| 357 |
+
"main_problem": "Continuous convulsions >5min",
|
| 358 |
+
"temp": "38.0°C",
|
| 359 |
+
"circulation": "Rapid pulse",
|
| 360 |
+
"cause": "Missed meds/High stress"
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"id": 31,
|
| 364 |
+
"situation": "Carbon Monoxide",
|
| 365 |
+
"chat_text": "Everyone in the cabin is lethargic with a headache. One person has bright red skin and is breathing very slowly. We suspect an exhaust leak.",
|
| 366 |
+
"responsive": "Lethargic",
|
| 367 |
+
"breathing": "Slow",
|
| 368 |
+
"pain": "5/10",
|
| 369 |
+
"main_problem": "Cherry-red skin, headache",
|
| 370 |
+
"temp": "36.6°C",
|
| 371 |
+
"circulation": "Normal",
|
| 372 |
+
"cause": "Leaking heater/engine exhaust"
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
"id": 32,
|
| 376 |
+
"situation": "Ciguatera Poisoning",
|
| 377 |
+
"chat_text": "We ate a barracuda and now he’s acting weird. He says cold water feels hot to him, and his heart rate has dropped to 40 beats per minute.",
|
| 378 |
+
"responsive": "Alert",
|
| 379 |
+
"breathing": "Normal",
|
| 380 |
+
"pain": "6/10",
|
| 381 |
+
"main_problem": "Hot feels cold, cold feels hot",
|
| 382 |
+
"temp": "37.2°C",
|
| 383 |
+
"circulation": "Bradycardia (Slow pulse)",
|
| 384 |
+
"cause": "Eating reef fish (Barracuda)"
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"id": 33,
|
| 388 |
+
"situation": "Box Jellyfish Sting",
|
| 389 |
+
"chat_text": "He was stung by a jellyfish and went into cardiac arrest almost immediately. There are massive red welts all over his chest and legs.",
|
| 390 |
+
"responsive": "Unconscious",
|
| 391 |
+
"breathing": "Arrested",
|
| 392 |
+
"pain": "10/10",
|
| 393 |
+
"main_problem": "Massive welts, heart failure",
|
| 394 |
+
"temp": "37.4°C",
|
| 395 |
+
"circulation": "Cardiac arrest signs",
|
| 396 |
+
"cause": "Swimming in doldrums"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"id": 34,
|
| 400 |
+
"situation": "Cellulitis",
|
| 401 |
+
"chat_text": "A small cut on her leg has turned into a massive, hot, red swelling that is spreading quickly. She has a high fever.",
|
| 402 |
+
"responsive": "Alert",
|
| 403 |
+
"breathing": "Normal",
|
| 404 |
+
"pain": "7/10",
|
| 405 |
+
"main_problem": "Leg hot, red, and swollen",
|
| 406 |
+
"temp": "39.0°C",
|
| 407 |
+
"circulation": "Fast pulse",
|
| 408 |
+
"cause": "Infected shaving cut"
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"id": 35,
|
| 412 |
+
"situation": "Chemical Burn (Eyes)",
|
| 413 |
+
"chat_text": "Battery acid splashed directly into his eyes. He can't open them, he’s in 10 out of 10 pain, and his eyes look hazy and white.",
|
| 414 |
+
"responsive": "Alert",
|
| 415 |
+
"breathing": "Normal",
|
| 416 |
+
"pain": "10/10",
|
| 417 |
+
"main_problem": "Cannot open eyes, white haze",
|
| 418 |
+
"temp": "36.8°C",
|
| 419 |
+
"circulation": "Normal",
|
| 420 |
+
"cause": "Battery acid splash"
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"id": 36,
|
| 424 |
+
"situation": "Aspiration Pneumonia",
|
| 425 |
+
"chat_text": "He’s been sick since he inhaled some vomit. He’s coughing up green gunk, has a high fever, and is struggling to breathe.",
|
| 426 |
+
"responsive": "Alert",
|
| 427 |
+
"breathing": "Labored",
|
| 428 |
+
"pain": "5/10",
|
| 429 |
+
"main_problem": "Productive cough (green)",
|
| 430 |
+
"temp": "39.2°C",
|
| 431 |
+
"circulation": "Low oxygen saturation",
|
| 432 |
+
"cause": "Vomit inhaled during storm"
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"id": 37,
|
| 436 |
+
"situation": "Acute Urinary Retention",
|
| 437 |
+
"chat_text": "He’s in extreme pain because he hasn't been able to pee for hours. His lower stomach is hard and bulging.",
|
| 438 |
+
"responsive": "Alert",
|
| 439 |
+
"breathing": "Normal",
|
| 440 |
+
"pain": "9/10",
|
| 441 |
+
"main_problem": "Bladder distended, cannot pee",
|
| 442 |
+
"temp": "37.1°C",
|
| 443 |
+
"circulation": "High BP",
|
| 444 |
+
"cause": "Enlarged prostate"
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"id": 38,
|
| 448 |
+
"situation": "Dental Abscess",
|
| 449 |
+
"chat_text": "His tooth is broken and now his entire face is swollen. His eye is starting to swell shut and he has a high fever.",
|
| 450 |
+
"responsive": "Alert",
|
| 451 |
+
"breathing": "Normal",
|
| 452 |
+
"pain": "8/10",
|
| 453 |
+
"main_problem": "Face swollen, eye closing",
|
| 454 |
+
"temp": "38.6°C",
|
| 455 |
+
"circulation": "Normal",
|
| 456 |
+
"cause": "Cracked tooth"
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"id": 39,
|
| 460 |
+
"situation": "Pulmonary Embolism",
|
| 461 |
+
"chat_text": "He suddenly got a sharp pain in his chest and can't breathe. His lips are blue and he’s coughing up a little bit of blood.",
|
| 462 |
+
"responsive": "Alert",
|
| 463 |
+
"breathing": "Sharp pain",
|
| 464 |
+
"pain": "9/10",
|
| 465 |
+
"main_problem": "Sudden SOB, coughing blood",
|
| 466 |
+
"temp": "37.4°C",
|
| 467 |
+
"circulation": "Cyanosis (Blue lips)",
|
| 468 |
+
"cause": "DVT from long sitting/watch"
|
| 469 |
+
},
|
| 470 |
+
{
|
| 471 |
+
"id": 40,
|
| 472 |
+
"situation": "Bowel Obstruction",
|
| 473 |
+
"chat_text": "He hasn't been able to go to the bathroom or pass gas. Now he is actually vomiting stuff that smells like a bowel movement.",
|
| 474 |
+
"responsive": "Alert",
|
| 475 |
+
"breathing": "Normal",
|
| 476 |
+
"pain": "7/10",
|
| 477 |
+
"main_problem": "Fecal vomiting, no gas",
|
| 478 |
+
"temp": "37.6°C",
|
| 479 |
+
"circulation": "Low BP, dehydrated",
|
| 480 |
+
"cause": "Previous surgery/Adhesions"
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"id": 41,
|
| 484 |
+
"situation": "Dengue Hemorrhagic",
|
| 485 |
+
"chat_text": "He has a high fever and his gums are bleeding. He’s covered in dark, blackish bruises and looks very weak.",
|
| 486 |
+
"responsive": "Drowsy",
|
| 487 |
+
"breathing": "Normal",
|
| 488 |
+
"pain": "7/10",
|
| 489 |
+
"main_problem": "Bleeding gums, black bruises",
|
| 490 |
+
"temp": "39.8°C",
|
| 491 |
+
"circulation": "Low BP (Shock)",
|
| 492 |
+
"cause": "Mosquito bite in Sumatra"
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"id": 42,
|
| 496 |
+
"situation": "Malaria (Falciparum)",
|
| 497 |
+
"chat_text": "He’s totally confused and has a 40.5°C fever. His eyes look yellow and he’s breathing very fast.",
|
| 498 |
+
"responsive": "Confused",
|
| 499 |
+
"breathing": "Rapid",
|
| 500 |
+
"pain": "6/10",
|
| 501 |
+
"main_problem": "Cycling high fever, yellow eyes",
|
| 502 |
+
"temp": "40.5°C",
|
| 503 |
+
"circulation": "Tachycardia",
|
| 504 |
+
"cause": "Mosquito bite"
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"id": 43,
|
| 508 |
+
"situation": "Giardia (Severe)",
|
| 509 |
+
"chat_text": "He has constant, explosive diarrhea that smells like sulfur. He’s becoming very dehydrated and lightheaded.",
|
| 510 |
+
"responsive": "Alert",
|
| 511 |
+
"breathing": "Normal",
|
| 512 |
+
"pain": "5/10",
|
| 513 |
+
"main_problem": "Explosive sulfurous diarrhea",
|
| 514 |
+
"temp": "37.5°C",
|
| 515 |
+
"circulation": "Orthostatic hypotension",
|
| 516 |
+
"cause": "Bad water tank hygiene"
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"id": 44,
|
| 520 |
+
"situation": "Corneal Ulcer",
|
| 521 |
+
"chat_text": "His eye is bright red and filled with pus. He says it feels like there is sand in it and he can't look at any light.",
|
| 522 |
+
"responsive": "Alert",
|
| 523 |
+
"breathing": "Normal",
|
| 524 |
+
"pain": "9/10",
|
| 525 |
+
"main_problem": "Constant sand sensation, pus",
|
| 526 |
+
"temp": "36.8°C",
|
| 527 |
+
"circulation": "Normal",
|
| 528 |
+
"cause": "Contact lens left in too long"
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"id": 45,
|
| 532 |
+
"situation": "Orchitis",
|
| 533 |
+
"chat_text": "He has sudden, 9 out of 10 pain in his groin. One side is swollen to the size of a grapefruit and he has a fever.",
|
| 534 |
+
"responsive": "Alert",
|
| 535 |
+
"breathing": "Normal",
|
| 536 |
+
"pain": "9/10",
|
| 537 |
+
"main_problem": "Scrotal swelling (grapefruit)",
|
| 538 |
+
"temp": "38.8°C",
|
| 539 |
+
"circulation": "Pain-induced high BP",
|
| 540 |
+
"cause": "Bacterial/STI"
|
| 541 |
+
},
|
| 542 |
+
{
|
| 543 |
+
"id": 46,
|
| 544 |
+
"situation": "Meningitis",
|
| 545 |
+
"chat_text": "He has a very high fever and a splitting headache. His neck is so stiff he can't touch his chin to his chest and light hurts his eyes.",
|
| 546 |
+
"responsive": "Lethargic",
|
| 547 |
+
"breathing": "Normal",
|
| 548 |
+
"pain": "9/10",
|
| 549 |
+
"main_problem": "Stiff neck, light sensitivity",
|
| 550 |
+
"temp": "40.1°C",
|
| 551 |
+
"circulation": "BP 100/60",
|
| 552 |
+
"cause": "Viral/Bacterial"
|
| 553 |
+
},
|
| 554 |
+
{
|
| 555 |
+
"id": 47,
|
| 556 |
+
"situation": "Staph (MRSA)",
|
| 557 |
+
"chat_text": "He has a huge, painful, pus-filled lump that looks like a spider bite. It’s hot to the touch and he has a fever.",
|
| 558 |
+
"responsive": "Alert",
|
| 559 |
+
"breathing": "Normal",
|
| 560 |
+
"pain": "6/10",
|
| 561 |
+
"main_problem": "\"Spider bite\" look, pus-filled",
|
| 562 |
+
"temp": "38.2°C",
|
| 563 |
+
"circulation": "Normal",
|
| 564 |
+
"cause": "Shared towels/gym gear"
|
| 565 |
+
},
|
| 566 |
+
{
|
| 567 |
+
"id": 48,
|
| 568 |
+
"situation": "Leptospirosis",
|
| 569 |
+
"chat_text": "His eyes are bright red, his skin looks yellow, and his calves are in a lot of pain. He has a high fever and low blood pressure.",
|
| 570 |
+
"responsive": "Alert",
|
| 571 |
+
"breathing": "Normal",
|
| 572 |
+
"pain": "8/10",
|
| 573 |
+
"main_problem": "Calf pain, jaundice, red eyes",
|
| 574 |
+
"temp": "39.4°C",
|
| 575 |
+
"circulation": "Low BP",
|
| 576 |
+
"cause": "Rat urine in bilge water"
|
| 577 |
+
},
|
| 578 |
+
{
|
| 579 |
+
"id": 49,
|
| 580 |
+
"situation": "Sea Urchin Granuloma",
|
| 581 |
+
"chat_text": "He stepped on a sea urchin and has over 20 spines stuck in his foot. His joints are starting to feel stiff and lock up.",
|
| 582 |
+
"responsive": "Alert",
|
| 583 |
+
"breathing": "Normal",
|
| 584 |
+
"pain": "4/10",
|
| 585 |
+
"main_problem": "20+ spines, joints locking",
|
| 586 |
+
"temp": "37.2°C",
|
| 587 |
+
"circulation": "Normal",
|
| 588 |
+
"cause": "Stepped on urchin in surf"
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"id": 50,
|
| 592 |
+
"situation": "Ectopic Pregnancy",
|
| 593 |
+
"chat_text": "She has a sudden, ripping pain in her lower stomach and just fainted. She is very pale, cold, and her blood pressure is very low.",
|
| 594 |
+
"responsive": "Alert",
|
| 595 |
+
"breathing": "Fast",
|
| 596 |
+
"pain": "10/10",
|
| 597 |
+
"main_problem": "Ripping pelvic pain, fainting",
|
| 598 |
+
"temp": "36.5°C",
|
| 599 |
+
"circulation": "BP 85/40 (Internal bleed)",
|
| 600 |
+
"cause": "Ruptured fallopian tube"
|
| 601 |
+
}
|
| 602 |
+
]
|
static/js/chat.js
CHANGED
|
@@ -6,9 +6,73 @@ let isProcessing = false;
|
|
| 6 |
let currentMode = 'triage';
|
| 7 |
const LAST_PROMPT_KEY = 'sailingmed:lastPrompt';
|
| 8 |
const LAST_PATIENT_KEY = 'sailingmed:lastPatient';
|
|
|
|
|
|
|
| 9 |
const PROMPT_PREVIEW_STATE_KEY = 'sailingmed:promptPreviewOpen';
|
| 10 |
const PROMPT_PREVIEW_CONTENT_KEY = 'sailingmed:promptPreviewContent';
|
| 11 |
const CHAT_STATE_KEY = 'sailingmed:chatState';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
function setupPromptInjectionPanel() {
|
| 14 |
const promptHeader = document.getElementById('prompt-preview-header');
|
|
@@ -76,6 +140,21 @@ if (document.readyState !== 'loading') {
|
|
| 76 |
document.addEventListener('DOMContentLoaded', () => applyChatState(currentMode), { once: true });
|
| 77 |
}
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
function updateUI() {
|
| 80 |
const banner = document.getElementById('banner');
|
| 81 |
const modeSelect = document.getElementById('mode-select');
|
|
@@ -207,6 +286,7 @@ function togglePriv() {
|
|
| 207 |
btn.style.background = isPrivate ? 'var(--triage)' : '#333';
|
| 208 |
btn.style.border = isPrivate ? '2px solid #fff' : '1px solid #222';
|
| 209 |
btn.innerText = isPrivate ? 'LOGGING: OFF' : 'LOGGING: ON';
|
|
|
|
| 210 |
updateUI();
|
| 211 |
}
|
| 212 |
|
|
@@ -217,14 +297,16 @@ async function runChat(promptText = null, force28b = false) {
|
|
| 217 |
isProcessing = true;
|
| 218 |
lastPrompt = txt;
|
| 219 |
const startTime = Date.now();
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
display.scrollTop = display.scrollHeight;
|
| 229 |
|
| 230 |
// Disable buttons
|
|
@@ -283,30 +365,77 @@ async function runChat(promptText = null, force28b = false) {
|
|
| 283 |
: (res.response || '').replace(/\n/g, '<br>');
|
| 284 |
const meta = res.model ? `[${res.model}${res.duration_ms ? ` · ${Math.round(res.duration_ms)} ms` : ` · ${Math.round(durationMs)} ms`}]` : '';
|
| 285 |
display.innerHTML += `<div class="response-block"><b>${meta}</b><br>${parsed}</div>`;
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
-
|
| 289 |
-
document.getElementById('msg').value = '';
|
| 290 |
-
}
|
| 291 |
persistChatState();
|
| 292 |
-
try {
|
|
|
|
|
|
|
|
|
|
| 293 |
if (display.lastElementChild) display.lastElementChild.scrollIntoView({behavior:'smooth'});
|
| 294 |
} catch (error) {
|
| 295 |
loadingDiv.remove();
|
| 296 |
display.innerHTML += `<div class="response-block" style="border-left-color:var(--red);"><b>ERROR:</b> ${error.message}</div>`;
|
| 297 |
} finally {
|
| 298 |
isProcessing = false;
|
|
|
|
|
|
|
| 299 |
document.getElementById('run-btn').disabled = false;
|
| 300 |
document.getElementById('repeat-btn').disabled = false;
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
-
function
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
}
|
|
|
|
| 310 |
}
|
| 311 |
|
| 312 |
// Handle Enter key for submission
|
|
@@ -324,6 +453,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 324 |
}
|
| 325 |
const savedPrompt = localStorage.getItem(LAST_PROMPT_KEY);
|
| 326 |
if (savedPrompt) lastPrompt = savedPrompt;
|
|
|
|
| 327 |
|
| 328 |
// Debug current patient select state
|
| 329 |
const pSelect = document.getElementById('p-select');
|
|
@@ -334,6 +464,109 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 334 |
}
|
| 335 |
});
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
function setMode(mode) {
|
| 338 |
const target = mode === 'inquiry' ? 'inquiry' : 'triage';
|
| 339 |
if (target === currentMode) return;
|
|
|
|
| 6 |
let currentMode = 'triage';
|
| 7 |
const LAST_PROMPT_KEY = 'sailingmed:lastPrompt';
|
| 8 |
const LAST_PATIENT_KEY = 'sailingmed:lastPatient';
|
| 9 |
+
const LAST_CHAT_MODE_KEY = 'sailingmed:lastChatMode';
|
| 10 |
+
const LOGGING_MODE_KEY = 'sailingmed:loggingOff';
|
| 11 |
const PROMPT_PREVIEW_STATE_KEY = 'sailingmed:promptPreviewOpen';
|
| 12 |
const PROMPT_PREVIEW_CONTENT_KEY = 'sailingmed:promptPreviewContent';
|
| 13 |
const CHAT_STATE_KEY = 'sailingmed:chatState';
|
| 14 |
+
let triageSamples = [];
|
| 15 |
+
|
| 16 |
+
function escapeHtml(str) {
|
| 17 |
+
return (str || '')
|
| 18 |
+
.replace(/&/g, '&')
|
| 19 |
+
.replace(/</g, '<')
|
| 20 |
+
.replace(/>/g, '>')
|
| 21 |
+
.replace(/"/g, '"')
|
| 22 |
+
.replace(/'/g, ''');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
async function loadTriageSamples() {
|
| 26 |
+
if (triageSamples.length) return triageSamples;
|
| 27 |
+
try {
|
| 28 |
+
const res = await fetch('/static/data/triage_samples.json', { cache: 'no-store' });
|
| 29 |
+
if (!res.ok) throw new Error(`Status ${res.status}`);
|
| 30 |
+
triageSamples = await res.json();
|
| 31 |
+
const select = document.getElementById('triage-sample-select');
|
| 32 |
+
if (select && Array.isArray(triageSamples)) {
|
| 33 |
+
triageSamples.forEach((s) => {
|
| 34 |
+
const opt = document.createElement('option');
|
| 35 |
+
opt.value = String(s.id);
|
| 36 |
+
opt.textContent = `${s.id}. ${s.situation}`;
|
| 37 |
+
select.appendChild(opt);
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
} catch (err) {
|
| 41 |
+
console.warn('Unable to load triage samples', err);
|
| 42 |
+
}
|
| 43 |
+
return triageSamples;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function setSelectValue(selectEl, value) {
|
| 47 |
+
if (!selectEl) return;
|
| 48 |
+
const match = Array.from(selectEl.options).find(o => o.value === value || o.textContent === value);
|
| 49 |
+
if (match) {
|
| 50 |
+
selectEl.value = match.value;
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
const opt = new Option(value, value, true, true);
|
| 54 |
+
selectEl.add(opt);
|
| 55 |
+
selectEl.value = value;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function applyTriageSample(sampleId) {
|
| 59 |
+
if (!sampleId) return;
|
| 60 |
+
const sample = triageSamples.find((s) => String(s.id) === String(sampleId));
|
| 61 |
+
if (!sample) return;
|
| 62 |
+
const msgTextarea = document.getElementById('msg');
|
| 63 |
+
if (msgTextarea) {
|
| 64 |
+
msgTextarea.value = sample.chat_text || '';
|
| 65 |
+
msgTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| 66 |
+
}
|
| 67 |
+
setSelectValue(document.getElementById('triage-consciousness'), sample.responsive || '');
|
| 68 |
+
setSelectValue(document.getElementById('triage-breathing-status'), sample.breathing || '');
|
| 69 |
+
setSelectValue(document.getElementById('triage-pain-level'), sample.pain || '');
|
| 70 |
+
setSelectValue(document.getElementById('triage-main-problem'), sample.main_problem || '');
|
| 71 |
+
setSelectValue(document.getElementById('triage-temperature'), sample.temp || '');
|
| 72 |
+
setSelectValue(document.getElementById('triage-circulation'), sample.circulation || '');
|
| 73 |
+
setSelectValue(document.getElementById('triage-cause'), sample.cause || '');
|
| 74 |
+
persistChatState();
|
| 75 |
+
}
|
| 76 |
|
| 77 |
function setupPromptInjectionPanel() {
|
| 78 |
const promptHeader = document.getElementById('prompt-preview-header');
|
|
|
|
| 140 |
document.addEventListener('DOMContentLoaded', () => applyChatState(currentMode), { once: true });
|
| 141 |
}
|
| 142 |
|
| 143 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 144 |
+
try {
|
| 145 |
+
const stored = localStorage.getItem(LOGGING_MODE_KEY);
|
| 146 |
+
if (stored === '1') {
|
| 147 |
+
isPrivate = true;
|
| 148 |
+
}
|
| 149 |
+
} catch (err) { /* ignore */ }
|
| 150 |
+
const btn = document.getElementById('priv-btn');
|
| 151 |
+
if (btn) {
|
| 152 |
+
btn.style.background = isPrivate ? 'var(--triage)' : '#333';
|
| 153 |
+
btn.style.border = isPrivate ? '2px solid #fff' : '1px solid #222';
|
| 154 |
+
btn.innerText = isPrivate ? 'LOGGING: OFF' : 'LOGGING: ON';
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
function updateUI() {
|
| 159 |
const banner = document.getElementById('banner');
|
| 160 |
const modeSelect = document.getElementById('mode-select');
|
|
|
|
| 286 |
btn.style.background = isPrivate ? 'var(--triage)' : '#333';
|
| 287 |
btn.style.border = isPrivate ? '2px solid #fff' : '1px solid #222';
|
| 288 |
btn.innerText = isPrivate ? 'LOGGING: OFF' : 'LOGGING: ON';
|
| 289 |
+
try { localStorage.setItem(LOGGING_MODE_KEY, isPrivate ? '1' : '0'); } catch (err) { /* ignore */ }
|
| 290 |
updateUI();
|
| 291 |
}
|
| 292 |
|
|
|
|
| 297 |
isProcessing = true;
|
| 298 |
lastPrompt = txt;
|
| 299 |
const startTime = Date.now();
|
| 300 |
+
const blocker = document.getElementById('chat-blocker');
|
| 301 |
+
if (blocker) blocker.style.display = 'flex';
|
| 302 |
|
| 303 |
+
// Show loading indicator
|
| 304 |
+
const display = document.getElementById('display');
|
| 305 |
+
const loadingDiv = document.createElement('div');
|
| 306 |
+
loadingDiv.id = 'loading-indicator';
|
| 307 |
+
loadingDiv.className = 'loading-indicator';
|
| 308 |
+
loadingDiv.innerHTML = '🔄 Analyzing...';
|
| 309 |
+
display.appendChild(loadingDiv);
|
| 310 |
display.scrollTop = display.scrollHeight;
|
| 311 |
|
| 312 |
// Disable buttons
|
|
|
|
| 365 |
: (res.response || '').replace(/\n/g, '<br>');
|
| 366 |
const meta = res.model ? `[${res.model}${res.duration_ms ? ` · ${Math.round(res.duration_ms)} ms` : ` · ${Math.round(durationMs)} ms`}]` : '';
|
| 367 |
display.innerHTML += `<div class="response-block"><b>${meta}</b><br>${parsed}</div>`;
|
| 368 |
+
if (typeof loadData === 'function') {
|
| 369 |
+
loadData(); // refresh crew history/logs after a chat completes
|
| 370 |
+
}
|
| 371 |
}
|
| 372 |
|
| 373 |
+
// Keep user-entered text so they can tweak/re-run
|
|
|
|
|
|
|
| 374 |
persistChatState();
|
| 375 |
+
try {
|
| 376 |
+
localStorage.setItem(LAST_PROMPT_KEY, lastPrompt);
|
| 377 |
+
localStorage.setItem(LAST_CHAT_MODE_KEY, currentMode);
|
| 378 |
+
} catch (err) { /* ignore storage issues */ }
|
| 379 |
if (display.lastElementChild) display.lastElementChild.scrollIntoView({behavior:'smooth'});
|
| 380 |
} catch (error) {
|
| 381 |
loadingDiv.remove();
|
| 382 |
display.innerHTML += `<div class="response-block" style="border-left-color:var(--red);"><b>ERROR:</b> ${error.message}</div>`;
|
| 383 |
} finally {
|
| 384 |
isProcessing = false;
|
| 385 |
+
const blocker = document.getElementById('chat-blocker');
|
| 386 |
+
if (blocker) blocker.style.display = 'none';
|
| 387 |
document.getElementById('run-btn').disabled = false;
|
| 388 |
document.getElementById('repeat-btn').disabled = false;
|
| 389 |
}
|
| 390 |
}
|
| 391 |
|
| 392 |
+
function restoreLast() {
|
| 393 |
+
// Restore last mode used
|
| 394 |
+
let modeToRestore = currentMode;
|
| 395 |
+
try {
|
| 396 |
+
const storedMode = localStorage.getItem(LAST_CHAT_MODE_KEY);
|
| 397 |
+
if (storedMode) modeToRestore = storedMode;
|
| 398 |
+
} catch (_) { /* ignore */ }
|
| 399 |
+
if (modeToRestore !== currentMode) {
|
| 400 |
+
setMode(modeToRestore);
|
| 401 |
+
}
|
| 402 |
+
const state = loadChatState();
|
| 403 |
+
const modeState = state[modeToRestore] || {};
|
| 404 |
+
const msgEl = document.getElementById('msg');
|
| 405 |
+
if (msgEl && typeof modeState.msg === 'string') {
|
| 406 |
+
msgEl.value = modeState.msg;
|
| 407 |
+
msgEl.focus();
|
| 408 |
+
}
|
| 409 |
+
if (modeToRestore === 'triage' && modeState.fields && typeof modeState.fields === 'object') {
|
| 410 |
+
Object.entries(modeState.fields).forEach(([id, val]) => {
|
| 411 |
+
const el = document.getElementById(id);
|
| 412 |
+
if (el) el.value = val;
|
| 413 |
+
});
|
| 414 |
+
}
|
| 415 |
+
// Restore patient selection
|
| 416 |
+
const patientSelect = document.getElementById('p-select');
|
| 417 |
+
if (patientSelect) {
|
| 418 |
+
const savedPatient = (() => {
|
| 419 |
+
try { return localStorage.getItem(LAST_PATIENT_KEY) || ''; } catch (_) { return ''; }
|
| 420 |
+
})();
|
| 421 |
+
if (savedPatient && Array.from(patientSelect.options).some(o => o.value === savedPatient)) {
|
| 422 |
+
patientSelect.value = savedPatient;
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
// Restore prompt preview if cached
|
| 426 |
+
const promptBox = document.getElementById('prompt-preview');
|
| 427 |
+
const promptHeader = document.getElementById('prompt-preview-header');
|
| 428 |
+
if (promptBox) {
|
| 429 |
+
try {
|
| 430 |
+
const cached = localStorage.getItem(PROMPT_PREVIEW_CONTENT_KEY);
|
| 431 |
+
if (cached) {
|
| 432 |
+
promptBox.value = cached;
|
| 433 |
+
promptBox.dataset.autofilled = 'false';
|
| 434 |
+
if (promptHeader) togglePromptPreviewArrow(promptHeader, true);
|
| 435 |
+
}
|
| 436 |
+
} catch (_) { /* ignore */ }
|
| 437 |
}
|
| 438 |
+
alert('Last chat restored to editor. You can revise and resend.');
|
| 439 |
}
|
| 440 |
|
| 441 |
// Handle Enter key for submission
|
|
|
|
| 453 |
}
|
| 454 |
const savedPrompt = localStorage.getItem(LAST_PROMPT_KEY);
|
| 455 |
if (savedPrompt) lastPrompt = savedPrompt;
|
| 456 |
+
loadTriageSamples().catch(() => {});
|
| 457 |
|
| 458 |
// Debug current patient select state
|
| 459 |
const pSelect = document.getElementById('p-select');
|
|
|
|
| 464 |
}
|
| 465 |
});
|
| 466 |
|
| 467 |
+
async function reactivateChat(historyId) {
|
| 468 |
+
try {
|
| 469 |
+
const res = await fetch('/api/data/history', { credentials: 'same-origin' });
|
| 470 |
+
if (!res.ok) throw new Error(`History load failed (${res.status})`);
|
| 471 |
+
const data = await res.json();
|
| 472 |
+
const entry = Array.isArray(data) ? data.find((h) => h.id === historyId) : null;
|
| 473 |
+
if (!entry) {
|
| 474 |
+
alert('Unable to reactivate: history entry not found.');
|
| 475 |
+
return;
|
| 476 |
+
}
|
| 477 |
+
const sameThread = Array.isArray(data)
|
| 478 |
+
? data
|
| 479 |
+
.filter((h) => {
|
| 480 |
+
const samePatient = (entry.patient_id && h.patient_id === entry.patient_id) || (entry.patient && h.patient === entry.patient);
|
| 481 |
+
const sameMode = (entry.mode || 'triage') === (h.mode || 'triage');
|
| 482 |
+
return samePatient && sameMode;
|
| 483 |
+
})
|
| 484 |
+
.sort((a, b) => (a.date || '').localeCompare(b.date || ''))
|
| 485 |
+
: [entry];
|
| 486 |
+
const transcript = sameThread
|
| 487 |
+
.map((h, idx) => {
|
| 488 |
+
const title = h.date ? `Entry ${idx + 1} — ${h.date}` : `Entry ${idx + 1}`;
|
| 489 |
+
return `${title}\nQUERY:\n${h.query || ''}\nRESPONSE:\n${h.response || ''}`;
|
| 490 |
+
})
|
| 491 |
+
.join('\n\n----\n\n');
|
| 492 |
+
// Set mode based on stored value if present
|
| 493 |
+
if (entry.mode) {
|
| 494 |
+
setMode(entry.mode);
|
| 495 |
+
}
|
| 496 |
+
// Navigate to the correct tab
|
| 497 |
+
const triageTabBtn = document.querySelector('.tab.tab-triage');
|
| 498 |
+
if (triageTabBtn) {
|
| 499 |
+
triageTabBtn.click();
|
| 500 |
+
}
|
| 501 |
+
// Attempt to restore patient selection using patient_id first, then name
|
| 502 |
+
const patientSelect = document.getElementById('p-select');
|
| 503 |
+
if (patientSelect) {
|
| 504 |
+
let targetVal = entry.patient_id || '';
|
| 505 |
+
if (targetVal && Array.from(patientSelect.options).some((o) => o.value === targetVal)) {
|
| 506 |
+
patientSelect.value = targetVal;
|
| 507 |
+
} else if (entry.patient) {
|
| 508 |
+
const matchByName = Array.from(patientSelect.options).find((o) => o.textContent === entry.patient);
|
| 509 |
+
if (matchByName) patientSelect.value = matchByName.value;
|
| 510 |
+
}
|
| 511 |
+
try { localStorage.setItem(LAST_PATIENT_KEY, patientSelect.value || ''); } catch (err) { /* ignore */ }
|
| 512 |
+
}
|
| 513 |
+
// Restore chat area with previous exchange
|
| 514 |
+
const display = document.getElementById('display');
|
| 515 |
+
if (display) {
|
| 516 |
+
const queryHtml = escapeHtml(entry.query || '').replace(/\n/g, '<br>');
|
| 517 |
+
const respHtml = escapeHtml(entry.response || '').replace(/\n/g, '<br>');
|
| 518 |
+
display.innerHTML = `
|
| 519 |
+
<div class="response-block" style="border-left-color:var(--inquiry);">
|
| 520 |
+
<b>Reactivated Chat</b><br>
|
| 521 |
+
<div style="margin-top:6px;"><strong>Query:</strong><br>${queryHtml}</div>
|
| 522 |
+
<div style="margin-top:6px;"><strong>Response:</strong><br>${respHtml}</div>
|
| 523 |
+
</div>`;
|
| 524 |
+
display.scrollTop = display.scrollHeight;
|
| 525 |
+
}
|
| 526 |
+
// Prefill message box with last query so user can continue
|
| 527 |
+
const msgTextarea = document.getElementById('msg');
|
| 528 |
+
if (msgTextarea) {
|
| 529 |
+
msgTextarea.value = entry.query || '';
|
| 530 |
+
msgTextarea.focus();
|
| 531 |
+
}
|
| 532 |
+
// Restore prompt (injected) view if available
|
| 533 |
+
const promptBox = document.getElementById('prompt-preview');
|
| 534 |
+
const promptHeader = document.getElementById('prompt-preview-header');
|
| 535 |
+
if (promptBox && entry.prompt) {
|
| 536 |
+
promptBox.value = entry.prompt;
|
| 537 |
+
promptBox.dataset.autofilled = 'false';
|
| 538 |
+
try { localStorage.setItem(PROMPT_PREVIEW_CONTENT_KEY, entry.prompt); } catch (err) { /* ignore */ }
|
| 539 |
+
if (promptHeader) {
|
| 540 |
+
togglePromptPreviewArrow(promptHeader, true);
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
// Inject transcript into prompt preview so the model sees prior context
|
| 544 |
+
if (promptBox) {
|
| 545 |
+
const transcriptBlock = transcript ? `Previous chat transcript:\n${transcript}` : '';
|
| 546 |
+
const combined = [transcriptBlock, promptBox.value].filter(Boolean).join('\n\n');
|
| 547 |
+
promptBox.value = combined;
|
| 548 |
+
promptBox.dataset.autofilled = 'false';
|
| 549 |
+
try { localStorage.setItem(PROMPT_PREVIEW_CONTENT_KEY, combined); } catch (err) { /* ignore */ }
|
| 550 |
+
if (promptHeader) {
|
| 551 |
+
togglePromptPreviewArrow(promptHeader, true);
|
| 552 |
+
}
|
| 553 |
+
const container = document.getElementById('prompt-preview-container');
|
| 554 |
+
if (container) container.style.display = 'block';
|
| 555 |
+
}
|
| 556 |
+
// Persist last prompt locally
|
| 557 |
+
if (entry.query) {
|
| 558 |
+
lastPrompt = entry.query;
|
| 559 |
+
try { localStorage.setItem(LAST_PROMPT_KEY, entry.query); } catch (err) { /* ignore */ }
|
| 560 |
+
}
|
| 561 |
+
alert('Chat restored. You can continue the conversation.');
|
| 562 |
+
} catch (err) {
|
| 563 |
+
alert(`Unable to reactivate chat: ${err.message}`);
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
window.reactivateChat = reactivateChat;
|
| 568 |
+
window.applyTriageSample = applyTriageSample;
|
| 569 |
+
|
| 570 |
function setMode(mode) {
|
| 571 |
const target = mode === 'inquiry' ? 'inquiry' : 'triage';
|
| 572 |
if (target === currentMode) return;
|
static/js/crew.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
// Reuse the chat dropdown storage key without redefining the global constant
|
| 4 |
const CREW_LAST_PATIENT_KEY = typeof LAST_PATIENT_KEY !== 'undefined' ? LAST_PATIENT_KEY : 'sailingmed:lastPatient';
|
|
|
|
| 5 |
|
| 6 |
function escapeHtml(str) {
|
| 7 |
return (str || '')
|
|
@@ -125,11 +126,16 @@ function groupHistoryByPatient(history) {
|
|
| 125 |
if (item && item.id) {
|
| 126 |
historyStoreById[item.id] = item;
|
| 127 |
}
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
if (
|
| 131 |
-
if (
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
});
|
| 134 |
return map;
|
| 135 |
}
|
|
@@ -154,6 +160,7 @@ function renderHistoryEntries(entries) {
|
|
| 154 |
<span class="toggle-label history-arrow" style="font-size:16px; margin-right:8px;">▸</span>
|
| 155 |
<span style="flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600; font-size:13px;">${date || 'Entry'}${preview ? ' — ' + preview : ''}</span>
|
| 156 |
<div style="display:flex; gap:6px; align-items:center;">
|
|
|
|
| 157 |
<button class="btn btn-sm history-entry-action" style="background:var(--inquiry); visibility:hidden;" onclick="event.stopPropagation(); exportHistoryItemById('${item.id || ''}')">Export</button>
|
| 158 |
<button class="btn btn-sm history-entry-action" style="background:var(--red); visibility:hidden;" onclick="event.stopPropagation(); deleteHistoryItemById('${item.id || ''}')">Delete</button>
|
| 159 |
</div>
|
|
@@ -190,14 +197,119 @@ function getHistoryForCrew(p, historyMap) {
|
|
| 190 |
if (fullName) keys.push(fullName);
|
| 191 |
if (p.name) keys.push(p.name);
|
| 192 |
if (p.firstName || p.lastName) keys.push(`${p.firstName || ''} ${p.lastName || ''}`.trim());
|
|
|
|
| 193 |
for (const k of keys) {
|
| 194 |
if (historyMap[k]) return historyMap[k];
|
| 195 |
}
|
| 196 |
return [];
|
| 197 |
}
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
// Load crew data for both medical and vessel/crew info
|
| 200 |
-
function loadCrewData(data, history = []) {
|
| 201 |
if (!Array.isArray(data)) {
|
| 202 |
console.warn('loadCrewData expected array, got', data);
|
| 203 |
return;
|
|
@@ -318,6 +430,10 @@ function loadCrewData(data, history = []) {
|
|
| 318 |
const ageStr = calculateAge(p.birthdate);
|
| 319 |
const posInfo = p.position ? ` • ${p.position}` : '';
|
| 320 |
const info = `${displayName}${ageStr}${posInfo}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
// Check if crew has data to determine default collapse state
|
| 323 |
const hasData = p.firstName && p.lastName && p.citizenship;
|
|
@@ -387,18 +503,54 @@ function loadCrewData(data, history = []) {
|
|
| 387 |
<div style="margin-bottom:8px; font-size:13px;">
|
| 388 |
<input type="text" id="phone-${p.id}" value="${p.phoneNumber || ''}" placeholder="Cell/WhatsApp Number" onchange="autoSaveProfile('${p.id}')" style="padding:5px; width:100%;">
|
| 389 |
</div>
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
</div>
|
| 396 |
-
<div style="
|
| 397 |
-
<
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
</div>
|
| 401 |
-
</div>
|
| 402 |
</div>
|
| 403 |
</div>
|
| 404 |
</div>
|
|
@@ -464,6 +616,7 @@ async function addCrew() {
|
|
| 464 |
emergencyContactEmail: emergencyContactEmail,
|
| 465 |
emergencyContactNotes: emergencyContactNotes,
|
| 466 |
phoneNumber: phoneNumber,
|
|
|
|
| 467 |
passportPhoto: '',
|
| 468 |
passportPage: '',
|
| 469 |
history: ''
|
|
@@ -494,6 +647,75 @@ async function addCrew() {
|
|
| 494 |
loadData();
|
| 495 |
}
|
| 496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
// Auto-save profile (debounced to prevent excessive saves)
|
| 498 |
let saveTimers = {};
|
| 499 |
async function autoSaveProfile(id) {
|
|
@@ -826,6 +1048,12 @@ async function loadVesselInfo() {
|
|
| 826 |
setVal('vessel-tonnage', v.tonnage);
|
| 827 |
setVal('vessel-net-tonnage', v.netTonnage);
|
| 828 |
setVal('vessel-mmsi', v.mmsi);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
}
|
| 830 |
|
| 831 |
async function saveVesselInfo() {
|
|
@@ -837,7 +1065,13 @@ async function saveVesselInfo() {
|
|
| 837 |
callSign: document.getElementById('vessel-callsign')?.value || '',
|
| 838 |
tonnage: document.getElementById('vessel-tonnage')?.value || '',
|
| 839 |
netTonnage: document.getElementById('vessel-net-tonnage')?.value || '',
|
| 840 |
-
mmsi: document.getElementById('vessel-mmsi')?.value || ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 841 |
};
|
| 842 |
await fetch('/api/data/vessel', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(v), credentials:'same-origin'});
|
| 843 |
alert('Vessel information saved.');
|
|
|
|
| 2 |
|
| 3 |
// Reuse the chat dropdown storage key without redefining the global constant
|
| 4 |
const CREW_LAST_PATIENT_KEY = typeof LAST_PATIENT_KEY !== 'undefined' ? LAST_PATIENT_KEY : 'sailingmed:lastPatient';
|
| 5 |
+
const DEFAULT_VACCINE_TYPES = [];
|
| 6 |
|
| 7 |
function escapeHtml(str) {
|
| 8 |
return (str || '')
|
|
|
|
| 126 |
if (item && item.id) {
|
| 127 |
historyStoreById[item.id] = item;
|
| 128 |
}
|
| 129 |
+
const keys = [];
|
| 130 |
+
const patientName = (item.patient || '').trim();
|
| 131 |
+
if (patientName) keys.push(patientName);
|
| 132 |
+
if (item.patient_id) keys.push(`id:${item.patient_id}`);
|
| 133 |
+
if (!patientName && !item.patient_id) keys.push('Unnamed Crew');
|
| 134 |
+
if (patientName && patientName.toLowerCase() === 'inquiry') keys.push('Inquiry History');
|
| 135 |
+
keys.forEach((k) => {
|
| 136 |
+
if (!map[k]) map[k] = [];
|
| 137 |
+
map[k].push(item);
|
| 138 |
+
});
|
| 139 |
});
|
| 140 |
return map;
|
| 141 |
}
|
|
|
|
| 160 |
<span class="toggle-label history-arrow" style="font-size:16px; margin-right:8px;">▸</span>
|
| 161 |
<span style="flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600; font-size:13px;">${date || 'Entry'}${preview ? ' — ' + preview : ''}</span>
|
| 162 |
<div style="display:flex; gap:6px; align-items:center;">
|
| 163 |
+
<button class="btn btn-sm history-entry-action" style="background:#3949ab; visibility:hidden;" onclick="event.stopPropagation(); reactivateChat('${item.id || ''}')">↩ Reactivate</button>
|
| 164 |
<button class="btn btn-sm history-entry-action" style="background:var(--inquiry); visibility:hidden;" onclick="event.stopPropagation(); exportHistoryItemById('${item.id || ''}')">Export</button>
|
| 165 |
<button class="btn btn-sm history-entry-action" style="background:var(--red); visibility:hidden;" onclick="event.stopPropagation(); deleteHistoryItemById('${item.id || ''}')">Delete</button>
|
| 166 |
</div>
|
|
|
|
| 197 |
if (fullName) keys.push(fullName);
|
| 198 |
if (p.name) keys.push(p.name);
|
| 199 |
if (p.firstName || p.lastName) keys.push(`${p.firstName || ''} ${p.lastName || ''}`.trim());
|
| 200 |
+
if (p.id) keys.push(`id:${p.id}`);
|
| 201 |
for (const k of keys) {
|
| 202 |
if (historyMap[k]) return historyMap[k];
|
| 203 |
}
|
| 204 |
return [];
|
| 205 |
}
|
| 206 |
|
| 207 |
+
function getVaccineOptions(settings = {}) {
|
| 208 |
+
const raw = Array.isArray(settings.vaccine_types) ? settings.vaccine_types : DEFAULT_VACCINE_TYPES;
|
| 209 |
+
const seen = new Set();
|
| 210 |
+
return raw
|
| 211 |
+
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
| 212 |
+
.filter((v) => !!v)
|
| 213 |
+
.filter((v) => {
|
| 214 |
+
const key = v.toLowerCase();
|
| 215 |
+
if (seen.has(key)) return false;
|
| 216 |
+
seen.add(key);
|
| 217 |
+
return true;
|
| 218 |
+
});
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function renderVaccineDetails(v) {
|
| 222 |
+
const fields = [
|
| 223 |
+
['Vaccine Type/Disease', v.vaccineType],
|
| 224 |
+
['Date Administered', v.dateAdministered],
|
| 225 |
+
['Dose Number', v.doseNumber],
|
| 226 |
+
['Next Dose Due Date', v.nextDoseDue],
|
| 227 |
+
['Trade Name & Manufacturer', v.tradeNameManufacturer],
|
| 228 |
+
['Lot/Batch Number', v.lotNumber],
|
| 229 |
+
['Administering Clinic/Provider', v.provider],
|
| 230 |
+
['Clinic/Provider Country', v.providerCountry],
|
| 231 |
+
['Expiration Date (dose)', v.expirationDate],
|
| 232 |
+
['Site & Route', v.siteRoute],
|
| 233 |
+
['Allergic Reactions', v.reactions],
|
| 234 |
+
['Remarks', v.remarks],
|
| 235 |
+
];
|
| 236 |
+
const rows = fields
|
| 237 |
+
.filter(([, val]) => val)
|
| 238 |
+
.map(([label, val]) => `<div><strong>${escapeHtml(label)}:</strong> ${escapeHtml(val)}</div>`)
|
| 239 |
+
.join('');
|
| 240 |
+
return rows || '<div style="color:#666;">No details recorded for this dose.</div>';
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function renderVaccineList(vaccines = [], crewId) {
|
| 244 |
+
if (!Array.isArray(vaccines) || vaccines.length === 0) {
|
| 245 |
+
return '<div style="font-size:12px; color:#666;">No vaccines recorded.</div>';
|
| 246 |
+
}
|
| 247 |
+
return vaccines
|
| 248 |
+
.map((v) => {
|
| 249 |
+
const vid = escapeHtml(v.id || '');
|
| 250 |
+
const label = escapeHtml(v.vaccineType || 'Vaccine');
|
| 251 |
+
const date = escapeHtml(v.dateAdministered || '');
|
| 252 |
+
return `
|
| 253 |
+
<div class="collapsible" style="margin-bottom:8px;">
|
| 254 |
+
<div class="col-header crew-med-header" onclick="toggleCrewSection(this)" style="justify-content:flex-start; background:#fff;">
|
| 255 |
+
<span class="dev-tag">dev:crew-vax-entry</span>
|
| 256 |
+
<span class="toggle-label history-arrow" style="font-size:16px; margin-right:8px;">▸</span>
|
| 257 |
+
<span style="flex:1; font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${label}${date ? ' — ' + date : ''}</span>
|
| 258 |
+
<button onclick="event.stopPropagation(); deleteVaccine('${crewId}', '${vid}')" class="btn btn-sm history-action-btn" style="background:var(--red); visibility:hidden;">🗑 Delete</button>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="col-body" style="padding:10px; display:none; font-size:12px; background:#f9fbff; border:1px solid #e0e7ff; border-top:none;">
|
| 261 |
+
${renderVaccineDetails(v)}
|
| 262 |
+
</div>
|
| 263 |
+
</div>`;
|
| 264 |
+
})
|
| 265 |
+
.join('');
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function clearVaccineInputs(crewId) {
|
| 269 |
+
const ids = [
|
| 270 |
+
`vx-type-${crewId}`,
|
| 271 |
+
`vx-type-other-${crewId}`,
|
| 272 |
+
`vx-date-${crewId}`,
|
| 273 |
+
`vx-dose-${crewId}`,
|
| 274 |
+
`vx-trade-${crewId}`,
|
| 275 |
+
`vx-lot-${crewId}`,
|
| 276 |
+
`vx-provider-${crewId}`,
|
| 277 |
+
`vx-provider-country-${crewId}`,
|
| 278 |
+
`vx-next-${crewId}`,
|
| 279 |
+
`vx-exp-${crewId}`,
|
| 280 |
+
`vx-site-${crewId}`,
|
| 281 |
+
`vx-remarks-${crewId}`,
|
| 282 |
+
];
|
| 283 |
+
ids.forEach((id) => {
|
| 284 |
+
const el = document.getElementById(id);
|
| 285 |
+
if (el) {
|
| 286 |
+
el.value = '';
|
| 287 |
+
if (id.includes('type-other')) {
|
| 288 |
+
el.style.display = 'none';
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
});
|
| 292 |
+
const rx = document.getElementById(`vx-reactions-${crewId}`);
|
| 293 |
+
if (rx) rx.value = '';
|
| 294 |
+
const typeSelect = document.getElementById(`vx-type-${crewId}`);
|
| 295 |
+
if (typeSelect) typeSelect.value = '';
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
function handleVaccineTypeChange(crewId) {
|
| 299 |
+
const select = document.getElementById(`vx-type-${crewId}`);
|
| 300 |
+
const other = document.getElementById(`vx-type-other-${crewId}`);
|
| 301 |
+
if (!select || !other) return;
|
| 302 |
+
const showOther = select.value === '__other__';
|
| 303 |
+
other.style.display = showOther ? 'block' : 'none';
|
| 304 |
+
if (!showOther) {
|
| 305 |
+
other.value = '';
|
| 306 |
+
} else {
|
| 307 |
+
other.focus();
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
// Load crew data for both medical and vessel/crew info
|
| 312 |
+
function loadCrewData(data, history = [], settings = {}) {
|
| 313 |
if (!Array.isArray(data)) {
|
| 314 |
console.warn('loadCrewData expected array, got', data);
|
| 315 |
return;
|
|
|
|
| 430 |
const ageStr = calculateAge(p.birthdate);
|
| 431 |
const posInfo = p.position ? ` • ${p.position}` : '';
|
| 432 |
const info = `${displayName}${ageStr}${posInfo}`;
|
| 433 |
+
const vaccines = Array.isArray(p.vaccines) ? p.vaccines : [];
|
| 434 |
+
const vaccineOptions = getVaccineOptions(settings);
|
| 435 |
+
const vaccineOptionMarkup = vaccineOptions.map((opt) => `<option value="${escapeHtml(opt)}">${escapeHtml(opt)}</option>`).join('');
|
| 436 |
+
const vaccineList = renderVaccineList(vaccines, p.id);
|
| 437 |
|
| 438 |
// Check if crew has data to determine default collapse state
|
| 439 |
const hasData = p.firstName && p.lastName && p.citizenship;
|
|
|
|
| 503 |
<div style="margin-bottom:8px; font-size:13px;">
|
| 504 |
<input type="text" id="phone-${p.id}" value="${p.phoneNumber || ''}" placeholder="Cell/WhatsApp Number" onchange="autoSaveProfile('${p.id}')" style="padding:5px; width:100%;">
|
| 505 |
</div>
|
| 506 |
+
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px; margin-bottom:8px;">
|
| 507 |
+
<div style="border:1px solid #ddd; padding:8px; border-radius:4px; background:#f9f9f9;">
|
| 508 |
+
<label style="margin-bottom:4px; display:block; font-weight:bold; font-size:11px;">Passport Photo:</label>
|
| 509 |
+
${p.passportPhoto ? (p.passportPhoto.startsWith('data:image/') ? `<div style="margin-bottom:4px;"><img src="${p.passportPhoto}" style="max-width:100%; max-height:120px; border:1px solid #ccc; border-radius:4px; cursor:pointer;" onclick="window.open('${p.passportPhoto}', '_blank')"><div style="margin-top:4px;"><button onclick="deleteDocument('${p.id}', 'passportPhoto')" style="background:var(--red); color:white; border:none; padding:2px 8px; border-radius:3px; cursor:pointer; font-size:10px;">🗑 Delete</button></div></div>` : `<div style="margin-bottom:4px;"><a href="${p.passportPhoto}" target="_blank" style="color:var(--inquiry); font-size:11px;">📎 View PDF</a> | <button onclick="deleteDocument('${p.id}', 'passportPhoto')" style="background:none; border:none; color:var(--red); cursor:pointer; font-size:10px;">🗑</button></div>`) : ''}
|
| 510 |
+
<input type="file" id="pp-${p.id}" accept="image/*,.pdf" onchange="uploadDocument('${p.id}', 'passportPhoto', this)" style="font-size:10px; width:100%;">
|
| 511 |
+
</div>
|
| 512 |
+
<div style="border:1px solid #ddd; padding:8px; border-radius:4px; background:#f9f9f9;">
|
| 513 |
+
<label style="margin-bottom:4px; display:block; font-weight:bold; font-size:11px;">Passport Page Photo:</label>
|
| 514 |
+
${p.passportPage ? (p.passportPage.startsWith('data:image/') ? `<div style="margin-bottom:4px;"><img src="${p.passportPage}" style="max-width:100%; max-height:120px; border:1px solid #ccc; border-radius:4px; cursor:pointer;" onclick="window.open('${p.passportPage}', '_blank')"><div style="margin-top:4px;"><button onclick="deleteDocument('${p.id}', 'passportPage')" style="background:var(--red); color:white; border:none; padding:2px 8px; border-radius:3px; cursor:pointer; font-size:10px;">🗑 Delete</button></div></div>` : `<div style="margin-bottom:4px;"><a href="${p.passportPage}" target="_blank" style="color:var(--inquiry); font-size:11px;">📎 View PDF</a> | <button onclick="deleteDocument('${p.id}', 'passportPage')" style="background:none; border:none; color:var(--red); cursor:pointer; font-size:10px;">🗑</button></div>`) : ''}
|
| 515 |
+
<input type="file" id="ppg-${p.id}" accept="image/*,.pdf" onchange="uploadDocument('${p.id}', 'passportPage', this)" style="font-size:10px; width:100%;">
|
| 516 |
+
</div>
|
| 517 |
</div>
|
| 518 |
+
<div class="collapsible" style="margin-top:12px;">
|
| 519 |
+
<div class="col-header crew-med-header" onclick="toggleCrewSection(this)" style="background:#fff6e8; border:1px solid #f0d9a8; justify-content:flex-start;">
|
| 520 |
+
<span class="dev-tag">dev:crew-vax-shell</span>
|
| 521 |
+
<span class="toggle-label history-arrow" style="font-size:18px; margin-right:8px;">▸</span>
|
| 522 |
+
<span style="font-weight:700;">Crew Vaccines</span>
|
| 523 |
+
<span style="font-size:12px; color:#6a5b3a; margin-left:8px;">${vaccines.length} recorded</span>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="col-body" style="padding:10px; background:#fffdf7; border:1px solid #f0d9a8; border-top:none; display:none;">
|
| 526 |
+
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-vax-form</div>
|
| 527 |
+
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:8px; margin-bottom:8px; font-size:12px;">
|
| 528 |
+
<div style="grid-column: span 2;">
|
| 529 |
+
<label style="font-weight:700; font-size:12px;">Vaccine Type/Disease *</label>
|
| 530 |
+
<select id="vx-type-${p.id}" onchange="handleVaccineTypeChange('${p.id}')" style="width:100%; padding:6px;">
|
| 531 |
+
<option value="">Select or choose Other…</option>
|
| 532 |
+
${vaccineOptionMarkup}
|
| 533 |
+
<option value="__other__">Other (type below)</option>
|
| 534 |
+
</select>
|
| 535 |
+
<input id="vx-type-other-${p.id}" type="text" style="width:100%; padding:6px; margin-top:6px; display:none;" placeholder="Enter other vaccine type">
|
| 536 |
+
</div>
|
| 537 |
+
<div><label style="font-weight:700; font-size:12px;">Date Administered</label><input id="vx-date-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="26-Jan-2026"></div>
|
| 538 |
+
<div><label style="font-weight:700; font-size:12px;">Dose Number</label><input id="vx-dose-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Dose 1 of 3"></div>
|
| 539 |
+
<div><label style="font-weight:700; font-size:12px;">Next Dose Due Date</label><input id="vx-next-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="e.g., 10-Feb-2026"></div>
|
| 540 |
+
<div style="grid-column: span 2;"><label style="font-weight:700; font-size:12px;">Trade Name & Manufacturer</label><input id="vx-trade-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Adacel by Sanofi Pasteur"></div>
|
| 541 |
+
<div><label style="font-weight:700; font-size:12px;">Lot/Batch Number</label><input id="vx-lot-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Batch #12345X"></div>
|
| 542 |
+
<div><label style="font-weight:700; font-size:12px;">Administering Clinic/Provider</label><input id="vx-provider-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Harbor Medical Clinic, Dock 3"></div>
|
| 543 |
+
<div><label style="font-weight:700; font-size:12px;">Clinic/Provider Country</label><input id="vx-provider-country-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Spain"></div>
|
| 544 |
+
<div><label style="font-weight:700; font-size:12px;">Expiration Date (dose)</label><input id="vx-exp-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="e.g., 30-Dec-2026"></div>
|
| 545 |
+
<div><label style="font-weight:700; font-size:12px;">Site & Route</label><input id="vx-site-${p.id}" type="text" style="width:100%; padding:6px;" placeholder="Left Arm - IM"></div>
|
| 546 |
+
<div style="grid-column: span 1;"><label style="font-weight:700; font-size:12px;">Allergic Reactions</label><textarea id="vx-reactions-${p.id}" style="width:100%; padding:6px; min-height:60px;" placeholder="Redness, fever, swelling..."></textarea></div>
|
| 547 |
+
<div style="grid-column: span 1;"><label style="font-weight:700; font-size:12px;">Remarks</label><textarea id="vx-remarks-${p.id}" style="width:100%; padding:6px; min-height:60px;" placeholder="Notes, special handling, country requirements, follow-up instructions..."></textarea></div>
|
| 548 |
+
</div>
|
| 549 |
+
<button onclick="addVaccine('${p.id}')" class="btn btn-sm" style="background:var(--dark); width:100%;"><span class="dev-tag">dev:crew-vax-add</span>+ Add Vaccine</button>
|
| 550 |
+
<div class="dev-tag" style="margin:10px 0 6px;">dev:crew-vax-list</div>
|
| 551 |
+
<div id="vax-list-${p.id}">${vaccineList}</div>
|
| 552 |
+
</div>
|
| 553 |
</div>
|
|
|
|
| 554 |
</div>
|
| 555 |
</div>
|
| 556 |
</div>
|
|
|
|
| 616 |
emergencyContactEmail: emergencyContactEmail,
|
| 617 |
emergencyContactNotes: emergencyContactNotes,
|
| 618 |
phoneNumber: phoneNumber,
|
| 619 |
+
vaccines: [],
|
| 620 |
passportPhoto: '',
|
| 621 |
passportPage: '',
|
| 622 |
history: ''
|
|
|
|
| 647 |
loadData();
|
| 648 |
}
|
| 649 |
|
| 650 |
+
async function addVaccine(crewId) {
|
| 651 |
+
const getVal = (suffix) => document.getElementById(`vx-${suffix}-${crewId}`)?.value.trim() || '';
|
| 652 |
+
const typeSelect = document.getElementById(`vx-type-${crewId}`);
|
| 653 |
+
const selectedType = typeSelect ? typeSelect.value : '';
|
| 654 |
+
const otherVal = getVal('type-other');
|
| 655 |
+
const vaccineType = selectedType === '__other__' ? otherVal : selectedType;
|
| 656 |
+
if (!vaccineType) {
|
| 657 |
+
alert('Please enter Vaccine Type/Disease');
|
| 658 |
+
if (selectedType === '__other__') {
|
| 659 |
+
const otherField = document.getElementById(`vx-type-other-${crewId}`);
|
| 660 |
+
if (otherField) otherField.focus();
|
| 661 |
+
} else if (typeSelect) {
|
| 662 |
+
typeSelect.focus();
|
| 663 |
+
}
|
| 664 |
+
return;
|
| 665 |
+
}
|
| 666 |
+
const entry = {
|
| 667 |
+
id: `vax-${Date.now()}`,
|
| 668 |
+
vaccineType,
|
| 669 |
+
dateAdministered: getVal('date'),
|
| 670 |
+
doseNumber: getVal('dose'),
|
| 671 |
+
tradeNameManufacturer: getVal('trade'),
|
| 672 |
+
lotNumber: getVal('lot'),
|
| 673 |
+
provider: getVal('provider'),
|
| 674 |
+
providerCountry: getVal('provider-country'),
|
| 675 |
+
nextDoseDue: getVal('next'),
|
| 676 |
+
expirationDate: getVal('exp'),
|
| 677 |
+
siteRoute: getVal('site'),
|
| 678 |
+
reactions: document.getElementById(`vx-reactions-${crewId}`)?.value.trim() || '',
|
| 679 |
+
remarks: document.getElementById(`vx-remarks-${crewId}`)?.value.trim() || ''
|
| 680 |
+
};
|
| 681 |
+
|
| 682 |
+
const data = await (await fetch('/api/data/patients', { credentials: 'same-origin' })).json();
|
| 683 |
+
const patient = data.find((p) => p.id === crewId);
|
| 684 |
+
if (!patient) {
|
| 685 |
+
alert('Crew member not found.');
|
| 686 |
+
return;
|
| 687 |
+
}
|
| 688 |
+
patient.vaccines = Array.isArray(patient.vaccines) ? patient.vaccines : [];
|
| 689 |
+
patient.vaccines.push(entry);
|
| 690 |
+
await fetch('/api/data/patients', {
|
| 691 |
+
method: 'POST',
|
| 692 |
+
headers: { 'Content-Type': 'application/json' },
|
| 693 |
+
body: JSON.stringify(data),
|
| 694 |
+
credentials: 'same-origin',
|
| 695 |
+
});
|
| 696 |
+
clearVaccineInputs(crewId);
|
| 697 |
+
loadData();
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
async function deleteVaccine(crewId, vaccineId) {
|
| 701 |
+
if (!vaccineId) return;
|
| 702 |
+
if (!confirm('Delete this vaccine record?')) return;
|
| 703 |
+
const data = await (await fetch('/api/data/patients', { credentials: 'same-origin' })).json();
|
| 704 |
+
const patient = data.find((p) => p.id === crewId);
|
| 705 |
+
if (!patient || !Array.isArray(patient.vaccines)) {
|
| 706 |
+
alert('Vaccine record not found.');
|
| 707 |
+
return;
|
| 708 |
+
}
|
| 709 |
+
patient.vaccines = patient.vaccines.filter((v) => v.id !== vaccineId);
|
| 710 |
+
await fetch('/api/data/patients', {
|
| 711 |
+
method: 'POST',
|
| 712 |
+
headers: { 'Content-Type': 'application/json' },
|
| 713 |
+
body: JSON.stringify(data),
|
| 714 |
+
credentials: 'same-origin',
|
| 715 |
+
});
|
| 716 |
+
loadData();
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
// Auto-save profile (debounced to prevent excessive saves)
|
| 720 |
let saveTimers = {};
|
| 721 |
async function autoSaveProfile(id) {
|
|
|
|
| 1048 |
setVal('vessel-tonnage', v.tonnage);
|
| 1049 |
setVal('vessel-net-tonnage', v.netTonnage);
|
| 1050 |
setVal('vessel-mmsi', v.mmsi);
|
| 1051 |
+
setVal('vessel-hull-number', v.hullNumber);
|
| 1052 |
+
setVal('vessel-starboard-engine', v.starboardEngine);
|
| 1053 |
+
setVal('vessel-starboard-sn', v.starboardEngineSn);
|
| 1054 |
+
setVal('vessel-port-engine', v.portEngine);
|
| 1055 |
+
setVal('vessel-port-sn', v.portEngineSn);
|
| 1056 |
+
setVal('vessel-rib-sn', v.ribSn);
|
| 1057 |
}
|
| 1058 |
|
| 1059 |
async function saveVesselInfo() {
|
|
|
|
| 1065 |
callSign: document.getElementById('vessel-callsign')?.value || '',
|
| 1066 |
tonnage: document.getElementById('vessel-tonnage')?.value || '',
|
| 1067 |
netTonnage: document.getElementById('vessel-net-tonnage')?.value || '',
|
| 1068 |
+
mmsi: document.getElementById('vessel-mmsi')?.value || '',
|
| 1069 |
+
hullNumber: document.getElementById('vessel-hull-number')?.value || '',
|
| 1070 |
+
starboardEngine: document.getElementById('vessel-starboard-engine')?.value || '',
|
| 1071 |
+
starboardEngineSn: document.getElementById('vessel-starboard-sn')?.value || '',
|
| 1072 |
+
portEngine: document.getElementById('vessel-port-engine')?.value || '',
|
| 1073 |
+
portEngineSn: document.getElementById('vessel-port-sn')?.value || '',
|
| 1074 |
+
ribSn: document.getElementById('vessel-rib-sn')?.value || ''
|
| 1075 |
};
|
| 1076 |
await fetch('/api/data/vessel', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(v), credentials:'same-origin'});
|
| 1077 |
alert('Vessel information saved.');
|
static/js/equipment.js
CHANGED
|
@@ -1005,6 +1005,10 @@ async function addMedicationItem() {
|
|
| 1005 |
return;
|
| 1006 |
}
|
| 1007 |
const category = getNewEquipmentVal('med-new-cat') || 'Medication';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
const exclude = document.getElementById('med-new-exclude')?.checked || false;
|
| 1009 |
const newId = `med-${Date.now()}`;
|
| 1010 |
const newMed = {
|
|
@@ -1024,6 +1028,8 @@ async function addMedicationItem() {
|
|
| 1024 |
primaryIndication: '',
|
| 1025 |
allergyWarnings: '',
|
| 1026 |
standardDosage: '',
|
|
|
|
|
|
|
| 1027 |
photos: [],
|
| 1028 |
purchaseHistory: [],
|
| 1029 |
source: 'manual_entry',
|
|
@@ -1044,6 +1050,12 @@ async function addMedicationItem() {
|
|
| 1044 |
|
| 1045 |
['med-new-name','med-new-cat','med-new-loc','med-new-subloc','med-new-parent','med-new-exp','med-new-inspect','med-new-batt','med-new-cal','med-new-qty','med-new-par','med-new-sup','med-new-notes']
|
| 1046 |
.forEach((id) => { const el = document.getElementById(id); if (el) el.value = ''; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
const medExclude = document.getElementById('med-new-exclude');
|
| 1048 |
if (medExclude) medExclude.checked = false;
|
| 1049 |
const typeSel = document.getElementById('med-new-type');
|
|
|
|
| 1005 |
return;
|
| 1006 |
}
|
| 1007 |
const category = getNewEquipmentVal('med-new-cat') || 'Medication';
|
| 1008 |
+
const sortSel = document.getElementById('med-new-sort');
|
| 1009 |
+
const sortCustom = document.getElementById('med-new-sort-custom');
|
| 1010 |
+
const sortCategory = sortSel ? (sortSel.value === 'Other' ? (sortCustom?.value || '') : (sortSel.value || '')) : '';
|
| 1011 |
+
const verified = !!document.getElementById('med-new-verified')?.checked;
|
| 1012 |
const exclude = document.getElementById('med-new-exclude')?.checked || false;
|
| 1013 |
const newId = `med-${Date.now()}`;
|
| 1014 |
const newMed = {
|
|
|
|
| 1028 |
primaryIndication: '',
|
| 1029 |
allergyWarnings: '',
|
| 1030 |
standardDosage: '',
|
| 1031 |
+
sortCategory,
|
| 1032 |
+
verified,
|
| 1033 |
photos: [],
|
| 1034 |
purchaseHistory: [],
|
| 1035 |
source: 'manual_entry',
|
|
|
|
| 1050 |
|
| 1051 |
['med-new-name','med-new-cat','med-new-loc','med-new-subloc','med-new-parent','med-new-exp','med-new-inspect','med-new-batt','med-new-cal','med-new-qty','med-new-par','med-new-sup','med-new-notes']
|
| 1052 |
.forEach((id) => { const el = document.getElementById(id); if (el) el.value = ''; });
|
| 1053 |
+
const sortSelect = document.getElementById('med-new-sort');
|
| 1054 |
+
const sortCustomInput = document.getElementById('med-new-sort-custom');
|
| 1055 |
+
if (sortSelect) sortSelect.value = '';
|
| 1056 |
+
if (sortCustomInput) { sortCustomInput.value = ''; sortCustomInput.style.display = 'none'; }
|
| 1057 |
+
const medVerified = document.getElementById('med-new-verified');
|
| 1058 |
+
if (medVerified) medVerified.checked = false;
|
| 1059 |
const medExclude = document.getElementById('med-new-exclude');
|
| 1060 |
if (medExclude) medExclude.checked = false;
|
| 1061 |
const typeSel = document.getElementById('med-new-type');
|
static/js/main.js
CHANGED
|
@@ -146,18 +146,26 @@ async function showTab(e, n) {
|
|
| 146 |
async function loadData() {
|
| 147 |
console.log('[DEBUG] loadData: start');
|
| 148 |
try {
|
| 149 |
-
const [res, historyRes] = await Promise.all([
|
| 150 |
fetch('/api/data/patients', { credentials: 'same-origin' }),
|
| 151 |
fetch('/api/data/history', { credentials: 'same-origin' }),
|
|
|
|
| 152 |
]);
|
| 153 |
-
console.log('[DEBUG] loadData: status patients', res.status, 'history', historyRes.status);
|
| 154 |
if (!res.ok) throw new Error(`Patients request failed: ${res.status}`);
|
| 155 |
if (!historyRes.ok) throw new Error(`History request failed: ${historyRes.status}`);
|
|
|
|
| 156 |
const data = await res.json();
|
| 157 |
const history = await historyRes.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
console.log('[DEBUG] loadData: patients length', Array.isArray(data) ? data.length : 'n/a');
|
| 159 |
if (!Array.isArray(data)) throw new Error('Unexpected patients data format');
|
| 160 |
-
loadCrewData(data, Array.isArray(history) ? history : []);
|
| 161 |
} catch (err) {
|
| 162 |
console.error('[DEBUG] Failed to load crew data', err);
|
| 163 |
// Gracefully clear UI to avoid JS errors
|
|
|
|
| 146 |
async function loadData() {
|
| 147 |
console.log('[DEBUG] loadData: start');
|
| 148 |
try {
|
| 149 |
+
const [res, historyRes, settingsRes] = await Promise.all([
|
| 150 |
fetch('/api/data/patients', { credentials: 'same-origin' }),
|
| 151 |
fetch('/api/data/history', { credentials: 'same-origin' }),
|
| 152 |
+
fetch('/api/data/settings', { credentials: 'same-origin' })
|
| 153 |
]);
|
| 154 |
+
console.log('[DEBUG] loadData: status patients', res.status, 'history', historyRes.status, 'settings', settingsRes.status);
|
| 155 |
if (!res.ok) throw new Error(`Patients request failed: ${res.status}`);
|
| 156 |
if (!historyRes.ok) throw new Error(`History request failed: ${historyRes.status}`);
|
| 157 |
+
if (!settingsRes.ok) console.warn('Settings request failed:', settingsRes.status);
|
| 158 |
const data = await res.json();
|
| 159 |
const history = await historyRes.json();
|
| 160 |
+
let settings = {};
|
| 161 |
+
try {
|
| 162 |
+
settings = await settingsRes.json();
|
| 163 |
+
} catch (err) {
|
| 164 |
+
console.warn('Settings parse failed, using defaults.', err);
|
| 165 |
+
}
|
| 166 |
console.log('[DEBUG] loadData: patients length', Array.isArray(data) ? data.length : 'n/a');
|
| 167 |
if (!Array.isArray(data)) throw new Error('Unexpected patients data format');
|
| 168 |
+
loadCrewData(data, Array.isArray(history) ? history : [], settings || {});
|
| 169 |
} catch (err) {
|
| 170 |
console.error('[DEBUG] Failed to load crew data', err);
|
| 171 |
// Gracefully clear UI to avoid JS errors
|
static/js/pharmacy.js
CHANGED
|
@@ -117,6 +117,28 @@ function getTextareaHeights() {
|
|
| 117 |
return map;
|
| 118 |
}
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
function ensurePurchaseDefaults(p) {
|
| 121 |
return {
|
| 122 |
id: p.id || `ph-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
@@ -145,6 +167,8 @@ function ensurePharmacyDefaults(item) {
|
|
| 145 |
primaryIndication: item.primaryIndication || '',
|
| 146 |
allergyWarnings: item.allergyWarnings || '',
|
| 147 |
standardDosage: item.standardDosage || '',
|
|
|
|
|
|
|
| 148 |
photos: Array.isArray(item.photos) ? item.photos : [],
|
| 149 |
purchaseHistory: Array.isArray(item.purchaseHistory)
|
| 150 |
? item.purchaseHistory.map(ensurePurchaseDefaults)
|
|
@@ -155,12 +179,12 @@ function ensurePharmacyDefaults(item) {
|
|
| 155 |
};
|
| 156 |
}
|
| 157 |
|
| 158 |
-
function scheduleSaveMedication(id) {
|
| 159 |
if (pharmacySaveTimers[id]) {
|
| 160 |
clearTimeout(pharmacySaveTimers[id]);
|
| 161 |
}
|
| 162 |
pharmacySaveTimers[id] = setTimeout(() => {
|
| 163 |
-
saveMedication(id);
|
| 164 |
}, 400);
|
| 165 |
}
|
| 166 |
|
|
@@ -180,13 +204,17 @@ function getMedicationDisplayName(med) {
|
|
| 180 |
function sortPharmacyItems(items) {
|
| 181 |
const list = Array.isArray(items) ? [...items] : [];
|
| 182 |
const sortSel = document.getElementById('pharmacy-sort');
|
| 183 |
-
const mode = (sortSel && sortSel.value) || '
|
| 184 |
const byText = (a, b, pathA, pathB) => {
|
| 185 |
const va = (pathA || '').toLowerCase();
|
| 186 |
const vb = (pathB || '').toLowerCase();
|
| 187 |
return va.localeCompare(vb);
|
| 188 |
};
|
| 189 |
list.sort((a, b) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
if (mode === 'brand') {
|
| 191 |
return byText(a, b, a.brandName || '', b.brandName || '');
|
| 192 |
}
|
|
@@ -224,6 +252,12 @@ async function loadPharmacy() {
|
|
| 224 |
}
|
| 225 |
}
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
function getOpenMedIds() {
|
| 228 |
return Array.from(document.querySelectorAll('#pharmacy-list .history-item .col-body[data-med-id]'))
|
| 229 |
.filter((el) => el.style.display !== 'none')
|
|
@@ -296,6 +330,7 @@ function renderMedicationCard(med, isOpen = true, textHeights = {}) {
|
|
| 296 |
const headerNote = [lowStock ? 'Low Stock' : null, expirySoon ? 'Expiring Soon' : null].filter(Boolean).join(' · ');
|
| 297 |
const displayName = getMedicationDisplayName(med);
|
| 298 |
const strength = (med.strength || '').trim();
|
|
|
|
| 299 |
const bodyDisplay = isOpen ? 'display:block;' : 'display:none;';
|
| 300 |
const arrow = isOpen ? '▾' : '▸';
|
| 301 |
const headerBg = med.excludeFromResources ? '#ffecef' : '#eef7ff';
|
|
@@ -304,7 +339,10 @@ function renderMedicationCard(med, isOpen = true, textHeights = {}) {
|
|
| 304 |
const bodyBorderColor = med.excludeFromResources ? '#ffcfd0' : '#cfe9d5';
|
| 305 |
const badgeColor = med.excludeFromResources ? '#d32f2f' : '#2e7d32';
|
| 306 |
const badgeText = med.excludeFromResources ? 'Resource Currently Unavailable' : 'Resource Available';
|
| 307 |
-
const availabilityBadge = `<span style="
|
|
|
|
|
|
|
|
|
|
| 308 |
const doseHeight = textHeights[`dose-${med.id}`] ? `height:${textHeights[`dose-${med.id}`]};` : '';
|
| 309 |
const photoThumbs = (med.photos || []).map(
|
| 310 |
(src, idx) => `
|
|
@@ -322,9 +360,14 @@ function renderMedicationCard(med, isOpen = true, textHeights = {}) {
|
|
| 322 |
<span style="flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:700;">
|
| 323 |
${displayName}${strength ? ' — ' + strength : ''}
|
| 324 |
</span>
|
|
|
|
| 325 |
${headerNote ? `<span class="sidebar-pill" style="margin-right:8px; background:${lowStock ? '#ffebee' : '#fff7e0'}; color:${lowStock ? '#c62828' : '#b26a00'};">${headerNote}</span>` : ''}
|
| 326 |
<button onclick="event.stopPropagation(); deleteMedication('${med.id}')" class="btn btn-sm history-action-btn" style="background:var(--red); visibility:hidden;">🗑 Delete Medication</button>
|
| 327 |
-
${availabilityBadge}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
</div>
|
| 329 |
<div class="col-body" data-med-id="${med.id}" style="padding:12px; background:${bodyBg}; border:1px solid ${bodyBorderColor}; border-radius:6px; ${bodyDisplay}">
|
| 330 |
<div class="collapsible" style="margin-bottom:10px;">
|
|
@@ -335,10 +378,10 @@ function renderMedicationCard(med, isOpen = true, textHeights = {}) {
|
|
| 335 |
</div>
|
| 336 |
<div class="col-body" style="padding:10px; display:block;" id="details-${med.id}">
|
| 337 |
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
<div>
|
| 343 |
<label style="font-weight:700; font-size:12px;">Generic Name</label>
|
| 344 |
<input id="gn-${med.id}" type="text" value="${med.genericName}" style="width:100%; padding:8px;" oninput="scheduleSaveMedication('${med.id}')">
|
|
@@ -389,6 +432,21 @@ function renderMedicationCard(med, isOpen = true, textHeights = {}) {
|
|
| 389 |
<option value="true" ${med.controlled ? 'selected' : ''}>Yes</option>
|
| 390 |
</select>
|
| 391 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
<div>
|
| 393 |
<label style="font-weight:700; font-size:12px;">Manufacturer</label>
|
| 394 |
<input id="manu-${med.id}" type="text" value="${med.manufacturer}" style="width:100%; padding:8px;" oninput="scheduleSaveMedication('${med.id}')">
|
|
@@ -625,7 +683,7 @@ function addPurchaseEntry(medId) {
|
|
| 625 |
scheduleSaveMedication(medId);
|
| 626 |
}
|
| 627 |
|
| 628 |
-
async function saveMedication(id) {
|
| 629 |
const openMedIds = getOpenMedIds();
|
| 630 |
const textHeights = getTextareaHeights();
|
| 631 |
const data = await (await fetchInventory()).json();
|
|
@@ -655,6 +713,8 @@ async function saveMedication(id) {
|
|
| 655 |
med.allergyWarnings = document.getElementById(`alg-${id}`)?.value || '';
|
| 656 |
med.standardDosage = document.getElementById(`dose-${id}`)?.value || '';
|
| 657 |
med.excludeFromResources = !!document.getElementById(`exclude-${id}`)?.checked;
|
|
|
|
|
|
|
| 658 |
med.purchaseHistory = collectPurchaseEntries(id);
|
| 659 |
|
| 660 |
await fetchInventory({
|
|
@@ -663,7 +723,9 @@ async function saveMedication(id) {
|
|
| 663 |
body: JSON.stringify(meds),
|
| 664 |
});
|
| 665 |
pharmacyCache = meds;
|
| 666 |
-
|
|
|
|
|
|
|
| 667 |
}
|
| 668 |
|
| 669 |
function collectPurchaseEntries(medId) {
|
|
|
|
| 117 |
return map;
|
| 118 |
}
|
| 119 |
|
| 120 |
+
function handleSortCategoryChange(id) {
|
| 121 |
+
const select = document.getElementById(`sort-${id}`);
|
| 122 |
+
const custom = document.getElementById(`sort-custom-${id}`);
|
| 123 |
+
if (!select || !custom) return;
|
| 124 |
+
const val = select.value;
|
| 125 |
+
const isCustom = val === 'Other';
|
| 126 |
+
custom.style.display = isCustom ? 'block' : 'none';
|
| 127 |
+
if (!isCustom) {
|
| 128 |
+
custom.value = '';
|
| 129 |
+
}
|
| 130 |
+
scheduleSaveMedication(id, true);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function toggleCustomSortField() {
|
| 134 |
+
const sel = document.getElementById('med-new-sort');
|
| 135 |
+
const custom = document.getElementById('med-new-sort-custom');
|
| 136 |
+
if (!sel || !custom) return;
|
| 137 |
+
const isCustom = sel.value === 'Other';
|
| 138 |
+
custom.style.display = isCustom ? 'block' : 'none';
|
| 139 |
+
if (!isCustom) custom.value = '';
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
function ensurePurchaseDefaults(p) {
|
| 143 |
return {
|
| 144 |
id: p.id || `ph-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
|
|
| 167 |
primaryIndication: item.primaryIndication || '',
|
| 168 |
allergyWarnings: item.allergyWarnings || '',
|
| 169 |
standardDosage: item.standardDosage || '',
|
| 170 |
+
sortCategory: item.sortCategory || '',
|
| 171 |
+
verified: !!item.verified,
|
| 172 |
photos: Array.isArray(item.photos) ? item.photos : [],
|
| 173 |
purchaseHistory: Array.isArray(item.purchaseHistory)
|
| 174 |
? item.purchaseHistory.map(ensurePurchaseDefaults)
|
|
|
|
| 179 |
};
|
| 180 |
}
|
| 181 |
|
| 182 |
+
function scheduleSaveMedication(id, rerender = false) {
|
| 183 |
if (pharmacySaveTimers[id]) {
|
| 184 |
clearTimeout(pharmacySaveTimers[id]);
|
| 185 |
}
|
| 186 |
pharmacySaveTimers[id] = setTimeout(() => {
|
| 187 |
+
saveMedication(id, rerender);
|
| 188 |
}, 400);
|
| 189 |
}
|
| 190 |
|
|
|
|
| 204 |
function sortPharmacyItems(items) {
|
| 205 |
const list = Array.isArray(items) ? [...items] : [];
|
| 206 |
const sortSel = document.getElementById('pharmacy-sort');
|
| 207 |
+
const mode = (sortSel && sortSel.value) || 'sortCategory';
|
| 208 |
const byText = (a, b, pathA, pathB) => {
|
| 209 |
const va = (pathA || '').toLowerCase();
|
| 210 |
const vb = (pathB || '').toLowerCase();
|
| 211 |
return va.localeCompare(vb);
|
| 212 |
};
|
| 213 |
list.sort((a, b) => {
|
| 214 |
+
if (mode === 'sortCategory') {
|
| 215 |
+
const cat = byText(a, b, a.sortCategory || '', b.sortCategory || '');
|
| 216 |
+
if (cat !== 0) return cat;
|
| 217 |
+
}
|
| 218 |
if (mode === 'brand') {
|
| 219 |
return byText(a, b, a.brandName || '', b.brandName || '');
|
| 220 |
}
|
|
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
| 255 |
+
function handlePharmacySortChange() {
|
| 256 |
+
const openIds = getOpenMedIds();
|
| 257 |
+
const textHeights = getTextareaHeights();
|
| 258 |
+
renderPharmacy(pharmacyCache, openIds, textHeights);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
function getOpenMedIds() {
|
| 262 |
return Array.from(document.querySelectorAll('#pharmacy-list .history-item .col-body[data-med-id]'))
|
| 263 |
.filter((el) => el.style.display !== 'none')
|
|
|
|
| 330 |
const headerNote = [lowStock ? 'Low Stock' : null, expirySoon ? 'Expiring Soon' : null].filter(Boolean).join(' · ');
|
| 331 |
const displayName = getMedicationDisplayName(med);
|
| 332 |
const strength = (med.strength || '').trim();
|
| 333 |
+
const sortLabel = med.sortCategory ? `<span style="font-size:11px; color:#455a64; margin-right:8px;">${med.sortCategory}</span>` : '';
|
| 334 |
const bodyDisplay = isOpen ? 'display:block;' : 'display:none;';
|
| 335 |
const arrow = isOpen ? '▾' : '▸';
|
| 336 |
const headerBg = med.excludeFromResources ? '#ffecef' : '#eef7ff';
|
|
|
|
| 339 |
const bodyBorderColor = med.excludeFromResources ? '#ffcfd0' : '#cfe9d5';
|
| 340 |
const badgeColor = med.excludeFromResources ? '#d32f2f' : '#2e7d32';
|
| 341 |
const badgeText = med.excludeFromResources ? 'Resource Currently Unavailable' : 'Resource Available';
|
| 342 |
+
const availabilityBadge = `<span style="padding:2px 10px; border-radius:999px; background:${badgeColor}; color:#fff; font-size:11px; white-space:nowrap;">${badgeText}</span>`;
|
| 343 |
+
const verifiedBadge = med.verified
|
| 344 |
+
? `<span style="padding:2px 10px; border-radius:999px; background:#1b5e20; color:#fff; font-size:11px; white-space:nowrap;">Verified</span>`
|
| 345 |
+
: `<span style="padding:2px 10px; border-radius:999px; background:transparent; color:#1b5e20; font-size:11px; white-space:nowrap; border:1px dashed #b2c7b5;">Not verified</span>`;
|
| 346 |
const doseHeight = textHeights[`dose-${med.id}`] ? `height:${textHeights[`dose-${med.id}`]};` : '';
|
| 347 |
const photoThumbs = (med.photos || []).map(
|
| 348 |
(src, idx) => `
|
|
|
|
| 360 |
<span style="flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:700;">
|
| 361 |
${displayName}${strength ? ' — ' + strength : ''}
|
| 362 |
</span>
|
| 363 |
+
${sortLabel}
|
| 364 |
${headerNote ? `<span class="sidebar-pill" style="margin-right:8px; background:${lowStock ? '#ffebee' : '#fff7e0'}; color:${lowStock ? '#c62828' : '#b26a00'};">${headerNote}</span>` : ''}
|
| 365 |
<button onclick="event.stopPropagation(); deleteMedication('${med.id}')" class="btn btn-sm history-action-btn" style="background:var(--red); visibility:hidden;">🗑 Delete Medication</button>
|
| 366 |
+
<div style="display:flex; align-items:center; gap:6px; margin-left:8px;">${verifiedBadge}${availabilityBadge}</div>
|
| 367 |
+
<label style="display:flex; align-items:center; gap:4px; font-size:11px; margin-left:8px; padding:4px 8px; border:1px solid #c7ddff; border-radius:6px; background:#fff;" onclick="event.stopPropagation();">
|
| 368 |
+
<input id="ver-${med.id}" type="checkbox" ${med.verified ? 'checked' : ''} onchange="scheduleSaveMedication('${med.id}', true); event.stopPropagation();">
|
| 369 |
+
Verified
|
| 370 |
+
</label>
|
| 371 |
</div>
|
| 372 |
<div class="col-body" data-med-id="${med.id}" style="padding:12px; background:${bodyBg}; border:1px solid ${bodyBorderColor}; border-radius:6px; ${bodyDisplay}">
|
| 373 |
<div class="collapsible" style="margin-bottom:10px;">
|
|
|
|
| 378 |
</div>
|
| 379 |
<div class="col-body" style="padding:10px; display:block;" id="details-${med.id}">
|
| 380 |
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
| 381 |
+
<input id="exclude-${med.id}" type="checkbox" ${med.excludeFromResources ? 'checked' : ''} onchange="scheduleSaveMedication('${med.id}', true)">
|
| 382 |
+
<label style="font-size:12px; line-height:1.2; margin:0;">Resource Currently Unavailable</label>
|
| 383 |
+
</div>
|
| 384 |
+
<div style="display:grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap:10px; margin-bottom:10px;">
|
| 385 |
<div>
|
| 386 |
<label style="font-weight:700; font-size:12px;">Generic Name</label>
|
| 387 |
<input id="gn-${med.id}" type="text" value="${med.genericName}" style="width:100%; padding:8px;" oninput="scheduleSaveMedication('${med.id}')">
|
|
|
|
| 432 |
<option value="true" ${med.controlled ? 'selected' : ''}>Yes</option>
|
| 433 |
</select>
|
| 434 |
</div>
|
| 435 |
+
<div>
|
| 436 |
+
<label style="font-weight:700; font-size:12px;">Sort Category</label>
|
| 437 |
+
<select id="sort-${med.id}" style="width:100%; padding:8px;" onchange="handleSortCategoryChange('${med.id}')">
|
| 438 |
+
<option value="">Select...</option>
|
| 439 |
+
<option value="Antibiotic" ${med.sortCategory === 'Antibiotic' ? 'selected' : ''}>Antibiotic</option>
|
| 440 |
+
<option value="Analgesic" ${med.sortCategory === 'Analgesic' ? 'selected' : ''}>Analgesic</option>
|
| 441 |
+
<option value="Cardiac" ${med.sortCategory === 'Cardiac' ? 'selected' : ''}>Cardiac</option>
|
| 442 |
+
<option value="Respiratory" ${med.sortCategory === 'Respiratory' ? 'selected' : ''}>Respiratory</option>
|
| 443 |
+
<option value="Gastrointestinal" ${med.sortCategory === 'Gastrointestinal' ? 'selected' : ''}>Gastrointestinal</option>
|
| 444 |
+
<option value="Endocrine" ${med.sortCategory === 'Endocrine' ? 'selected' : ''}>Endocrine</option>
|
| 445 |
+
<option value="Emergency" ${med.sortCategory === 'Emergency' ? 'selected' : ''}>Emergency</option>
|
| 446 |
+
<option value="Other" ${med.sortCategory && !['Antibiotic','Analgesic','Cardiac','Respiratory','Gastrointestinal','Endocrine','Emergency'].includes(med.sortCategory) ? 'selected' : ''}>Custom...</option>
|
| 447 |
+
</select>
|
| 448 |
+
<input id="sort-custom-${med.id}" type="text" value="${(['Antibiotic','Analgesic','Cardiac','Respiratory','Gastrointestinal','Endocrine','Emergency'].includes(med.sortCategory) ? '' : med.sortCategory) || ''}" placeholder="Custom category" style="width:100%; padding:6px; margin-top:6px; ${med.sortCategory && !['Antibiotic','Analgesic','Cardiac','Respiratory','Gastrointestinal','Endocrine','Emergency'].includes(med.sortCategory) ? '' : 'display:none;'}" oninput="scheduleSaveMedication('${med.id}', true)">
|
| 449 |
+
</div>
|
| 450 |
<div>
|
| 451 |
<label style="font-weight:700; font-size:12px;">Manufacturer</label>
|
| 452 |
<input id="manu-${med.id}" type="text" value="${med.manufacturer}" style="width:100%; padding:8px;" oninput="scheduleSaveMedication('${med.id}')">
|
|
|
|
| 683 |
scheduleSaveMedication(medId);
|
| 684 |
}
|
| 685 |
|
| 686 |
+
async function saveMedication(id, rerender = false) {
|
| 687 |
const openMedIds = getOpenMedIds();
|
| 688 |
const textHeights = getTextareaHeights();
|
| 689 |
const data = await (await fetchInventory()).json();
|
|
|
|
| 713 |
med.allergyWarnings = document.getElementById(`alg-${id}`)?.value || '';
|
| 714 |
med.standardDosage = document.getElementById(`dose-${id}`)?.value || '';
|
| 715 |
med.excludeFromResources = !!document.getElementById(`exclude-${id}`)?.checked;
|
| 716 |
+
med.sortCategory = document.getElementById(`sort-${id}`)?.value || document.getElementById(`sort-custom-${id}`)?.value || '';
|
| 717 |
+
med.verified = !!document.getElementById(`ver-${id}`)?.checked;
|
| 718 |
med.purchaseHistory = collectPurchaseEntries(id);
|
| 719 |
|
| 720 |
await fetchInventory({
|
|
|
|
| 723 |
body: JSON.stringify(meds),
|
| 724 |
});
|
| 725 |
pharmacyCache = meds;
|
| 726 |
+
if (rerender) {
|
| 727 |
+
renderPharmacy(pharmacyCache, openMedIds, textHeights);
|
| 728 |
+
}
|
| 729 |
}
|
| 730 |
|
| 731 |
function collectPurchaseEntries(medId) {
|
static/js/settings.js
CHANGED
|
@@ -13,7 +13,8 @@ const DEFAULT_SETTINGS = {
|
|
| 13 |
rep_penalty: 1.1,
|
| 14 |
user_mode: "user",
|
| 15 |
med_photo_model: "qwen",
|
| 16 |
-
med_photo_prompt: "You are a pharmacy intake assistant on a sailing vessel. Look at the medication photo and return JSON only with keys: generic_name, brand_name, form, strength, expiry_date, batch_lot, storage_location, manufacturer, indication, allergy_warnings, dosage, notes."
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
let settingsDirty = false;
|
|
@@ -21,6 +22,7 @@ let settingsLoaded = false;
|
|
| 21 |
let settingsAutoSaveTimer = null;
|
| 22 |
let workspaceListLoaded = false;
|
| 23 |
let offlineStatusCache = null;
|
|
|
|
| 24 |
|
| 25 |
function setUserMode(mode) {
|
| 26 |
const body = document.body;
|
|
@@ -42,6 +44,8 @@ function setUserMode(mode) {
|
|
| 42 |
|
| 43 |
function applySettingsToUI(data = {}) {
|
| 44 |
const merged = { ...DEFAULT_SETTINGS, ...(data || {}) };
|
|
|
|
|
|
|
| 45 |
Object.keys(merged).forEach(k => {
|
| 46 |
const el = document.getElementById(k);
|
| 47 |
if (el) {
|
|
@@ -164,6 +168,7 @@ async function saveSettings(showAlert = true, reason = 'manual') {
|
|
| 164 |
s[k] = val;
|
| 165 |
}
|
| 166 |
});
|
|
|
|
| 167 |
console.log('[settings] saving', { reason, payload: s });
|
| 168 |
updateSettingsStatus('Saving…', false);
|
| 169 |
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -188,7 +193,7 @@ async function saveSettings(showAlert = true, reason = 'manual') {
|
|
| 188 |
const updated = await res.json();
|
| 189 |
console.log('[settings] save response', updated);
|
| 190 |
// Preserve the locally selected user_mode to avoid flicker if the server echoes stale data
|
| 191 |
-
const merged = { ...updated, user_mode: s.user_mode || updated.user_mode };
|
| 192 |
applySettingsToUI(merged);
|
| 193 |
try { localStorage.setItem('user_mode', updated.user_mode || 'user'); } catch (err) { /* ignore */ }
|
| 194 |
settingsDirty = false;
|
|
@@ -232,6 +237,77 @@ function resetSection(section) {
|
|
| 232 |
saveSettings();
|
| 233 |
}
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
function renderOfflineStatus(msg, isError = false) {
|
| 236 |
const box = document.getElementById('offline-status');
|
| 237 |
if (!box) return;
|
|
@@ -528,3 +604,6 @@ window.setUserMode = setUserMode;
|
|
| 528 |
window.runOfflineCheck = runOfflineCheck;
|
| 529 |
window.createOfflineBackup = createOfflineBackup;
|
| 530 |
window.restoreOfflineBackup = restoreOfflineBackup;
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
rep_penalty: 1.1,
|
| 14 |
user_mode: "user",
|
| 15 |
med_photo_model: "qwen",
|
| 16 |
+
med_photo_prompt: "You are a pharmacy intake assistant on a sailing vessel. Look at the medication photo and return JSON only with keys: generic_name, brand_name, form, strength, expiry_date, batch_lot, storage_location, manufacturer, indication, allergy_warnings, dosage, notes.",
|
| 17 |
+
vaccine_types: ["MMR", "DTaP", "HepB", "HepA", "Td/Tdap", "Influenza", "COVID-19"]
|
| 18 |
};
|
| 19 |
|
| 20 |
let settingsDirty = false;
|
|
|
|
| 22 |
let settingsAutoSaveTimer = null;
|
| 23 |
let workspaceListLoaded = false;
|
| 24 |
let offlineStatusCache = null;
|
| 25 |
+
let vaccineTypeList = [...DEFAULT_SETTINGS.vaccine_types];
|
| 26 |
|
| 27 |
function setUserMode(mode) {
|
| 28 |
const body = document.body;
|
|
|
|
| 44 |
|
| 45 |
function applySettingsToUI(data = {}) {
|
| 46 |
const merged = { ...DEFAULT_SETTINGS, ...(data || {}) };
|
| 47 |
+
vaccineTypeList = normalizeVaccineTypes(merged.vaccine_types);
|
| 48 |
+
renderVaccineTypes();
|
| 49 |
Object.keys(merged).forEach(k => {
|
| 50 |
const el = document.getElementById(k);
|
| 51 |
if (el) {
|
|
|
|
| 168 |
s[k] = val;
|
| 169 |
}
|
| 170 |
});
|
| 171 |
+
s.vaccine_types = normalizeVaccineTypes(vaccineTypeList);
|
| 172 |
console.log('[settings] saving', { reason, payload: s });
|
| 173 |
updateSettingsStatus('Saving…', false);
|
| 174 |
const headers = { 'Content-Type': 'application/json' };
|
|
|
|
| 193 |
const updated = await res.json();
|
| 194 |
console.log('[settings] save response', updated);
|
| 195 |
// Preserve the locally selected user_mode to avoid flicker if the server echoes stale data
|
| 196 |
+
const merged = { ...updated, user_mode: s.user_mode || updated.user_mode, vaccine_types: s.vaccine_types || updated.vaccine_types };
|
| 197 |
applySettingsToUI(merged);
|
| 198 |
try { localStorage.setItem('user_mode', updated.user_mode || 'user'); } catch (err) { /* ignore */ }
|
| 199 |
settingsDirty = false;
|
|
|
|
| 237 |
saveSettings();
|
| 238 |
}
|
| 239 |
|
| 240 |
+
function normalizeVaccineTypes(list) {
|
| 241 |
+
if (!Array.isArray(list)) return [...DEFAULT_SETTINGS.vaccine_types];
|
| 242 |
+
const seen = new Set();
|
| 243 |
+
return list
|
| 244 |
+
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
| 245 |
+
.filter((v) => !!v)
|
| 246 |
+
.filter((v) => {
|
| 247 |
+
const key = v.toLowerCase();
|
| 248 |
+
if (seen.has(key)) return false;
|
| 249 |
+
seen.add(key);
|
| 250 |
+
return true;
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function renderVaccineTypes() {
|
| 255 |
+
const container = document.getElementById('vaccine-types-list');
|
| 256 |
+
if (!container) return;
|
| 257 |
+
if (!vaccineTypeList.length) {
|
| 258 |
+
container.innerHTML = '<div style="color:#666; font-size:12px;">No vaccine types defined. Add at least one to enable the dropdown.</div>';
|
| 259 |
+
return;
|
| 260 |
+
}
|
| 261 |
+
container.innerHTML = vaccineTypeList
|
| 262 |
+
.map((v, idx) => `
|
| 263 |
+
<div style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid #eee;">
|
| 264 |
+
<div style="width:28px; text-align:right; font-weight:700; color:#666;">${idx + 1}.</div>
|
| 265 |
+
<div style="flex:1; font-weight:600;">${v}</div>
|
| 266 |
+
<div style="display:flex; gap:6px; align-items:center;">
|
| 267 |
+
<button class="btn btn-sm" style="background:#607d8b;" ${idx === 0 ? 'disabled' : ''} onclick="moveVaccineType(${idx}, -1)">↑</button>
|
| 268 |
+
<button class="btn btn-sm" style="background:#607d8b;" ${idx === vaccineTypeList.length - 1 ? 'disabled' : ''} onclick="moveVaccineType(${idx}, 1)">↓</button>
|
| 269 |
+
<button class="btn btn-sm" style="background:var(--red);" onclick="removeVaccineType(${idx})">🗑 Remove</button>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
`).join('');
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function addVaccineType() {
|
| 276 |
+
const input = document.getElementById('vaccine-type-input');
|
| 277 |
+
if (!input) return;
|
| 278 |
+
const val = input.value.trim();
|
| 279 |
+
if (!val) {
|
| 280 |
+
alert('Enter a vaccine type before adding.');
|
| 281 |
+
return;
|
| 282 |
+
}
|
| 283 |
+
vaccineTypeList = normalizeVaccineTypes([...vaccineTypeList, val]);
|
| 284 |
+
input.value = '';
|
| 285 |
+
renderVaccineTypes();
|
| 286 |
+
settingsDirty = true;
|
| 287 |
+
scheduleAutoSave('vaccine-type-add');
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
function removeVaccineType(idx) {
|
| 291 |
+
if (idx < 0 || idx >= vaccineTypeList.length) return;
|
| 292 |
+
vaccineTypeList.splice(idx, 1);
|
| 293 |
+
vaccineTypeList = normalizeVaccineTypes(vaccineTypeList);
|
| 294 |
+
renderVaccineTypes();
|
| 295 |
+
settingsDirty = true;
|
| 296 |
+
scheduleAutoSave('vaccine-type-remove');
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
function moveVaccineType(idx, delta) {
|
| 300 |
+
const newIndex = idx + delta;
|
| 301 |
+
if (newIndex < 0 || newIndex >= vaccineTypeList.length) return;
|
| 302 |
+
const nextList = [...vaccineTypeList];
|
| 303 |
+
const [item] = nextList.splice(idx, 1);
|
| 304 |
+
nextList.splice(newIndex, 0, item);
|
| 305 |
+
vaccineTypeList = normalizeVaccineTypes(nextList);
|
| 306 |
+
renderVaccineTypes();
|
| 307 |
+
settingsDirty = true;
|
| 308 |
+
scheduleAutoSave('vaccine-type-reorder');
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
function renderOfflineStatus(msg, isError = false) {
|
| 312 |
const box = document.getElementById('offline-status');
|
| 313 |
if (!box) return;
|
|
|
|
| 604 |
window.runOfflineCheck = runOfflineCheck;
|
| 605 |
window.createOfflineBackup = createOfflineBackup;
|
| 606 |
window.restoreOfflineBackup = restoreOfflineBackup;
|
| 607 |
+
window.addVaccineType = addVaccineType;
|
| 608 |
+
window.removeVaccineType = removeVaccineType;
|
| 609 |
+
window.moveVaccineType = moveVaccineType;
|
templates/index.html
CHANGED
|
@@ -184,6 +184,38 @@
|
|
| 184 |
.phone-block { display: block; }
|
| 185 |
.phone-import-panel { width: 100%; min-height: 130vh; }
|
| 186 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
</style>
|
| 188 |
<script>
|
| 189 |
window.WORKSPACE_LABEL = "{{ workspace.label if workspace else '' }}";
|
|
@@ -268,6 +300,12 @@
|
|
| 268 |
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
|
| 269 |
<span class="dev-tag">dev:triage-message</span>
|
| 270 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
<textarea id="msg" style="height:105px;" placeholder="Describe what's happening"></textarea>
|
| 272 |
<div id="triage-meta-selects" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:10px; margin-top:10px;">
|
| 273 |
<div>
|
|
@@ -368,7 +406,7 @@
|
|
| 368 |
</div>
|
| 369 |
<div style="display:flex; gap:10px; margin-top:10px; align-items:center; flex-wrap:wrap;">
|
| 370 |
<button onclick="runChat()" id="run-btn" class="btn" style="flex-grow:1; background:var(--triage); min-width:160px;">SUBMIT FOR TRIAGE</button>
|
| 371 |
-
<button onclick="
|
| 372 |
</div>
|
| 373 |
</div>
|
| 374 |
</div>
|
|
@@ -432,6 +470,10 @@
|
|
| 432 |
<span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Pharmaceuticals</span><span id="pharmacy-count" style="margin-left:8px; font-weight:700; font-size:12px; color:#1f2d3d;">(0)</span><span class="dev-tag">dev:medicines-shell</span>
|
| 433 |
</div>
|
| 434 |
<div class="col-body" style="padding:12px; display:none; background:#f5f8ff; border:1px solid #d7e2ff; border-top:none;">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
<div class="collapsible" style="margin-bottom:12px;">
|
| 436 |
<div id="medication-add-header" class="col-header crew-med-header" data-sidebar-id="equipment-medication-add" onclick="toggleSection(this)" style="background:#fff; justify-content:flex-start;">
|
| 437 |
<span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Add Pharmaceuticals</span><span class="dev-tag">dev:medication-add</span>
|
|
@@ -480,6 +522,25 @@
|
|
| 480 |
<option value="Damaged/Out of Service">Damaged/Out of Service</option>
|
| 481 |
</select>
|
| 482 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
<div>
|
| 484 |
<label style="font-weight:700; font-size:12px;">Expiry Date</label>
|
| 485 |
<input id="med-new-exp" type="date" style="width:100%; padding:8px;">
|
|
@@ -571,10 +632,20 @@
|
|
| 571 |
</div>
|
| 572 |
</div>
|
| 573 |
</div>
|
| 574 |
-
|
| 575 |
<div class="col-body" style="padding:12px; background:#f8f9fa; display:block;">
|
| 576 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-inventory</div>
|
| 577 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-controls</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-list</div>
|
| 579 |
<div id="pharmacy-list"></div>
|
| 580 |
</div>
|
|
@@ -693,6 +764,7 @@
|
|
| 693 |
<div class="page-body sidebar-open">
|
| 694 |
<div class="page-main">
|
| 695 |
<div class="page-shell">
|
|
|
|
| 696 |
<div class="panel-wrapper">
|
| 697 |
<div class="collapsible" style="margin-bottom:15px;">
|
| 698 |
<div class="col-header crew-med-header" data-sidebar-id="vessel-info" onclick="toggleSection(this)" style="background:#e3f2fd; border:1px solid #c5ddf8; justify-content:flex-start;">
|
|
@@ -702,39 +774,71 @@
|
|
| 702 |
<div class="dev-tag" style="margin-bottom:6px;">dev:vessel-fields</div>
|
| 703 |
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px; margin-bottom:12px; font-size:15px;">
|
| 704 |
<div>
|
|
|
|
| 705 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Vessel Name</label>
|
| 706 |
<input type="text" id="vessel-name" style="padding:8px; width:100%;">
|
| 707 |
</div>
|
| 708 |
<div>
|
|
|
|
| 709 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Registration Number</label>
|
| 710 |
<input type="text" id="vessel-registration" style="padding:8px; width:100%;">
|
| 711 |
</div>
|
| 712 |
<div>
|
|
|
|
| 713 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Flag Country</label>
|
| 714 |
<input type="text" id="vessel-flag" style="padding:8px; width:100%;">
|
| 715 |
</div>
|
| 716 |
<div>
|
|
|
|
| 717 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Home Port</label>
|
| 718 |
<input type="text" id="vessel-homeport" style="padding:8px; width:100%;">
|
| 719 |
</div>
|
| 720 |
<div>
|
|
|
|
| 721 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Gross Tonnage</label>
|
| 722 |
<input type="text" id="vessel-tonnage" style="padding:8px; width:100%;">
|
| 723 |
</div>
|
| 724 |
<div>
|
|
|
|
| 725 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Net/Register Tonnage</label>
|
| 726 |
<input type="text" id="vessel-net-tonnage" style="padding:8px; width:100%;">
|
| 727 |
</div>
|
| 728 |
<div>
|
|
|
|
| 729 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Call Sign</label>
|
| 730 |
<input type="text" id="vessel-callsign" style="padding:8px; width:100%;">
|
| 731 |
</div>
|
| 732 |
<div>
|
|
|
|
| 733 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">MMSI Number</label>
|
| 734 |
<input type="text" id="vessel-mmsi" style="padding:8px; width:100%;">
|
| 735 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
</div>
|
| 737 |
-
<button onclick="saveVesselInfo()" class="btn btn-sm" style="background:var(--inquiry); width:100%;">Save Vessel Info</button>
|
| 738 |
</div>
|
| 739 |
</div>
|
| 740 |
<div class="collapsible" style="margin-bottom:15px;">
|
|
@@ -749,12 +853,12 @@
|
|
| 749 |
<div class="col-body" style="padding:15px; background:#f8f9fa;">
|
| 750 |
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-add-fields</div>
|
| 751 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 752 |
-
<div><label style="font-size:13px; margin-bottom:2px;">First Name *</label><input type="text" id="cn-first" style="padding:6px; width:100%;"></div>
|
| 753 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Middle Name(s)</label><input type="text" id="cn-middle" style="padding:6px; width:100%;"></div>
|
| 754 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Last Name *</label><input type="text" id="cn-last" style="padding:6px; width:100%;"></div>
|
| 755 |
</div>
|
| 756 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 757 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Sex</label><select id="cn-sex" style="padding:6px; width:100%;">
|
| 758 |
<option value="">Select...</option>
|
| 759 |
<option value="Male">Male</option>
|
| 760 |
<option value="Female">Female</option>
|
|
@@ -762,8 +866,8 @@
|
|
| 762 |
<option value="Other">Other</option>
|
| 763 |
<option value="Prefer not to say">Prefer not to say</option>
|
| 764 |
</select></div>
|
| 765 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Birthdate</label><input type="date" id="cn-birthdate" style="padding:6px; width:100%;"></div>
|
| 766 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Position</label><select id="cn-position" style="padding:6px; width:100%;">
|
| 767 |
<option value="">Select...</option>
|
| 768 |
<option value="Captain">Captain</option>
|
| 769 |
<option value="Crew">Crew</option>
|
|
@@ -771,36 +875,38 @@
|
|
| 771 |
</select></div>
|
| 772 |
</div>
|
| 773 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 774 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Citizenship</label><input type="text" id="cn-citizenship" list="countries" style="padding:6px; width:100%;"></div>
|
| 775 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Birthplace</label><input type="text" id="cn-birthplace" list="countries" style="padding:6px; width:100%;"></div>
|
| 776 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Passport Number</label><input type="text" id="cn-passport" style="padding:6px; width:100%;"></div>
|
| 777 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Issue Date</label><input type="date" id="cn-pass-issue" style="padding:6px; width:100%;"></div>
|
| 778 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Expiry Date</label><input type="date" id="cn-pass-expiry" style="padding:6px; width:100%;"></div>
|
| 779 |
</div>
|
| 780 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 781 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Cell/WhatsApp</label><input type="text" id="cn-phone" placeholder="+1234567890" style="padding:6px; width:100%;"></div>
|
| 782 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Passport Photo/PDF</label><input type="file" id="cn-passport-photo" accept="image/*,.pdf" style="padding:4px; width:100%; font-size:13px;"></div>
|
| 783 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Passport Page Photo/PDF</label><input type="file" id="cn-passport-page" accept="image/*,.pdf" style="padding:4px; width:100%; font-size:13px;"></div>
|
| 784 |
</div>
|
| 785 |
<div style="margin-bottom:8px;"><label style="font-size:12px; font-weight:bold;">Emergency Contact</label></div>
|
| 786 |
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-add-emergency</div>
|
| 787 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 788 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Name</label><input type="text" id="cn-emerg-name" style="padding:6px; width:100%;"></div>
|
| 789 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Relationship</label><input type="text" id="cn-emerg-rel" style="padding:6px; width:100%;"></div>
|
| 790 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Phone</label><input type="text" id="cn-emerg-phone" style="padding:6px; width:100%;"></div>
|
| 791 |
-
<div><label style="font-size:13px; margin-bottom:2px;">Email</label><input type="email" id="cn-emerg-email" style="padding:6px; width:100%;"></div>
|
| 792 |
</div>
|
| 793 |
<div style="margin-bottom:10px; font-size:15px;">
|
| 794 |
<label style="font-size:13px; margin-bottom:2px;">Emergency Contact Notes</label>
|
|
|
|
| 795 |
<input type="text" id="cn-emerg-notes" placeholder="Additional emergency contact information" style="padding:6px; width:100%;">
|
| 796 |
</div>
|
| 797 |
<datalist id="countries">
|
| 798 |
<option value="USA"><option value="Canada"><option value="UK"><option value="Australia"><option value="New Zealand"><option value="France"><option value="Germany"><option value="Spain"><option value="Italy"><option value="Netherlands"><option value="Singapore"><option value="Malaysia"><option value="Thailand"><option value="Philippines"><option value="Japan"><option value="China"><option value="India">
|
| 799 |
</datalist>
|
| 800 |
-
<button onclick="addCrew()" class="btn btn-sm" style="background:var(--dark); width:100%;">+ Add Crew Member</button>
|
| 801 |
</div>
|
| 802 |
</div>
|
| 803 |
|
|
|
|
| 804 |
<div id="crew-info-list"></div>
|
| 805 |
</div>
|
| 806 |
</div>
|
|
@@ -849,6 +955,21 @@
|
|
| 849 |
<div style="font-size:12px; color:#555;">Ensures all required models are cached and lets you back up/restore the cache before going offline.</div>
|
| 850 |
</div>
|
| 851 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
<div class="collapsible developer-only" style="margin-bottom:12px; display:none;">
|
| 853 |
<div class="col-header crew-med-header" onclick="toggleSection(this)" style="background:#fff; justify-content:flex-start;">
|
| 854 |
<span class="dev-tag">dev:settings-default-export</span><span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Export Default Dataset (Dev)</span>
|
|
@@ -1017,11 +1138,19 @@
|
|
| 1017 |
</div>
|
| 1018 |
</div>
|
| 1019 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1020 |
<script src="/static/js/chat.js?v=2017"></script>
|
| 1021 |
-
<script src="/static/js/crew.js?v=
|
| 1022 |
<script src="/static/js/pharmacy.js?v=2026"></script>
|
| 1023 |
<script src="/static/js/equipment.js?v=2038"></script>
|
| 1024 |
-
<script src="/static/js/settings.js?v=
|
| 1025 |
-
<script src="/static/js/main.js?v=
|
| 1026 |
</body>
|
| 1027 |
</html>
|
|
|
|
| 184 |
.phone-block { display: block; }
|
| 185 |
.phone-import-panel { width: 100%; min-height: 130vh; }
|
| 186 |
}
|
| 187 |
+
/* Blocking modal while GPU chat is running */
|
| 188 |
+
#chat-blocker {
|
| 189 |
+
display: none;
|
| 190 |
+
position: fixed;
|
| 191 |
+
inset: 0;
|
| 192 |
+
background: rgba(0,0,0,0.65);
|
| 193 |
+
z-index: 9000;
|
| 194 |
+
align-items: center;
|
| 195 |
+
justify-content: center;
|
| 196 |
+
padding: 16px;
|
| 197 |
+
box-sizing: border-box;
|
| 198 |
+
}
|
| 199 |
+
#chat-blocker .modal {
|
| 200 |
+
background: #fff;
|
| 201 |
+
border-radius: 10px;
|
| 202 |
+
padding: 18px;
|
| 203 |
+
max-width: 520px;
|
| 204 |
+
width: 100%;
|
| 205 |
+
box-shadow: 0 12px 32px rgba(0,0,0,0.2);
|
| 206 |
+
text-align: center;
|
| 207 |
+
}
|
| 208 |
+
#chat-blocker h3 { margin: 0 0 10px 0; color: #1f2d3d; }
|
| 209 |
+
#chat-blocker p { margin: 8px 0; color: #2c3e50; line-height: 1.5; }
|
| 210 |
+
#chat-blocker .spinner {
|
| 211 |
+
width: 40px; height: 40px;
|
| 212 |
+
border-radius: 50%;
|
| 213 |
+
border: 4px solid #e0e0e0;
|
| 214 |
+
border-top-color: var(--inquiry);
|
| 215 |
+
margin: 14px auto;
|
| 216 |
+
animation: spin 1s linear infinite;
|
| 217 |
+
}
|
| 218 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 219 |
</style>
|
| 220 |
<script>
|
| 221 |
window.WORKSPACE_LABEL = "{{ workspace.label if workspace else '' }}";
|
|
|
|
| 300 |
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
|
| 301 |
<span class="dev-tag">dev:triage-message</span>
|
| 302 |
</div>
|
| 303 |
+
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
|
| 304 |
+
<label for="triage-sample-select" style="font-size:12px; color:#333; font-weight:700;">Load sample case:</label>
|
| 305 |
+
<select id="triage-sample-select" style="padding:8px; min-width:240px;" onchange="applyTriageSample(this.value)">
|
| 306 |
+
<option value="">Select a sample…</option>
|
| 307 |
+
</select>
|
| 308 |
+
</div>
|
| 309 |
<textarea id="msg" style="height:105px;" placeholder="Describe what's happening"></textarea>
|
| 310 |
<div id="triage-meta-selects" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:10px; margin-top:10px;">
|
| 311 |
<div>
|
|
|
|
| 406 |
</div>
|
| 407 |
<div style="display:flex; gap:10px; margin-top:10px; align-items:center; flex-wrap:wrap;">
|
| 408 |
<button onclick="runChat()" id="run-btn" class="btn" style="flex-grow:1; background:var(--triage); min-width:160px;">SUBMIT FOR TRIAGE</button>
|
| 409 |
+
<button onclick="restoreLast()" id="repeat-btn" class="btn user-adv-only" style="background:#555; min-width:120px;">RESTORE LAST</button>
|
| 410 |
</div>
|
| 411 |
</div>
|
| 412 |
</div>
|
|
|
|
| 470 |
<span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Pharmaceuticals</span><span id="pharmacy-count" style="margin-left:8px; font-weight:700; font-size:12px; color:#1f2d3d;">(0)</span><span class="dev-tag">dev:medicines-shell</span>
|
| 471 |
</div>
|
| 472 |
<div class="col-body" style="padding:12px; display:none; background:#f5f8ff; border:1px solid #d7e2ff; border-top:none;">
|
| 473 |
+
<div class="developer-only" style="display:none; margin-bottom:8px; display:flex; gap:8px; flex-wrap:wrap;">
|
| 474 |
+
<button class="btn btn-sm" style="background:#0b8457;" onclick="exportPharmacyJSON()">Export Pharmaceuticals JSON</button>
|
| 475 |
+
<button class="btn btn-sm" style="background:#1b4f72;" onclick="importPharmacyJSON()">Import Pharmaceuticals JSON</button>
|
| 476 |
+
</div>
|
| 477 |
<div class="collapsible" style="margin-bottom:12px;">
|
| 478 |
<div id="medication-add-header" class="col-header crew-med-header" data-sidebar-id="equipment-medication-add" onclick="toggleSection(this)" style="background:#fff; justify-content:flex-start;">
|
| 479 |
<span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Add Pharmaceuticals</span><span class="dev-tag">dev:medication-add</span>
|
|
|
|
| 522 |
<option value="Damaged/Out of Service">Damaged/Out of Service</option>
|
| 523 |
</select>
|
| 524 |
</div>
|
| 525 |
+
<div>
|
| 526 |
+
<label style="font-weight:700; font-size:12px;">Sort Category</label>
|
| 527 |
+
<select id="med-new-sort" style="width:100%; padding:8px;" onchange="toggleCustomSortField()">
|
| 528 |
+
<option value="">Select...</option>
|
| 529 |
+
<option value="Antibiotic">Antibiotic</option>
|
| 530 |
+
<option value="Analgesic">Analgesic</option>
|
| 531 |
+
<option value="Cardiac">Cardiac</option>
|
| 532 |
+
<option value="Respiratory">Respiratory</option>
|
| 533 |
+
<option value="Gastrointestinal">Gastrointestinal</option>
|
| 534 |
+
<option value="Endocrine">Endocrine</option>
|
| 535 |
+
<option value="Emergency">Emergency</option>
|
| 536 |
+
<option value="Other">Custom...</option>
|
| 537 |
+
</select>
|
| 538 |
+
<input id="med-new-sort-custom" type="text" placeholder="Custom category" style="margin-top:6px; width:100%; padding:8px; display:none;">
|
| 539 |
+
</div>
|
| 540 |
+
<div style="display:flex; align-items:center; gap:6px;">
|
| 541 |
+
<input id="med-new-verified" type="checkbox">
|
| 542 |
+
<label style="font-size:12px; margin:0;">Verified</label>
|
| 543 |
+
</div>
|
| 544 |
<div>
|
| 545 |
<label style="font-weight:700; font-size:12px;">Expiry Date</label>
|
| 546 |
<input id="med-new-exp" type="date" style="width:100%; padding:8px;">
|
|
|
|
| 632 |
</div>
|
| 633 |
</div>
|
| 634 |
</div>
|
| 635 |
+
<div class="collapsible" style="margin-bottom:12px;">
|
| 636 |
<div class="col-body" style="padding:12px; background:#f8f9fa; display:block;">
|
| 637 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-inventory</div>
|
| 638 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-controls</div>
|
| 639 |
+
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:8px;">
|
| 640 |
+
<label style="font-size:12px; font-weight:700;">Sort by:</label>
|
| 641 |
+
<select id="pharmacy-sort" style="padding:8px; min-width:180px;" onchange="handlePharmacySortChange()">
|
| 642 |
+
<option value="sortCategory">Sort Category</option>
|
| 643 |
+
<option value="generic">Generic Name</option>
|
| 644 |
+
<option value="brand">Brand Name</option>
|
| 645 |
+
<option value="strength">Strength</option>
|
| 646 |
+
<option value="expiry">Expiry Date</option>
|
| 647 |
+
</select>
|
| 648 |
+
</div>
|
| 649 |
<div class="dev-tag" style="margin-bottom:6px;">dev:pharmacy-list</div>
|
| 650 |
<div id="pharmacy-list"></div>
|
| 651 |
</div>
|
|
|
|
| 764 |
<div class="page-body sidebar-open">
|
| 765 |
<div class="page-main">
|
| 766 |
<div class="page-shell">
|
| 767 |
+
<div class="dev-tag" style="margin-bottom:8px;">dev:vessel-crew-tab</div>
|
| 768 |
<div class="panel-wrapper">
|
| 769 |
<div class="collapsible" style="margin-bottom:15px;">
|
| 770 |
<div class="col-header crew-med-header" data-sidebar-id="vessel-info" onclick="toggleSection(this)" style="background:#e3f2fd; border:1px solid #c5ddf8; justify-content:flex-start;">
|
|
|
|
| 774 |
<div class="dev-tag" style="margin-bottom:6px;">dev:vessel-fields</div>
|
| 775 |
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px; margin-bottom:12px; font-size:15px;">
|
| 776 |
<div>
|
| 777 |
+
<div class="dev-tag">dev:vessel-name</div>
|
| 778 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Vessel Name</label>
|
| 779 |
<input type="text" id="vessel-name" style="padding:8px; width:100%;">
|
| 780 |
</div>
|
| 781 |
<div>
|
| 782 |
+
<div class="dev-tag">dev:vessel-registration</div>
|
| 783 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Registration Number</label>
|
| 784 |
<input type="text" id="vessel-registration" style="padding:8px; width:100%;">
|
| 785 |
</div>
|
| 786 |
<div>
|
| 787 |
+
<div class="dev-tag">dev:vessel-flag</div>
|
| 788 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Flag Country</label>
|
| 789 |
<input type="text" id="vessel-flag" style="padding:8px; width:100%;">
|
| 790 |
</div>
|
| 791 |
<div>
|
| 792 |
+
<div class="dev-tag">dev:vessel-homeport</div>
|
| 793 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Home Port</label>
|
| 794 |
<input type="text" id="vessel-homeport" style="padding:8px; width:100%;">
|
| 795 |
</div>
|
| 796 |
<div>
|
| 797 |
+
<div class="dev-tag">dev:vessel-tonnage</div>
|
| 798 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Gross Tonnage</label>
|
| 799 |
<input type="text" id="vessel-tonnage" style="padding:8px; width:100%;">
|
| 800 |
</div>
|
| 801 |
<div>
|
| 802 |
+
<div class="dev-tag">dev:vessel-net-tonnage</div>
|
| 803 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Net/Register Tonnage</label>
|
| 804 |
<input type="text" id="vessel-net-tonnage" style="padding:8px; width:100%;">
|
| 805 |
</div>
|
| 806 |
<div>
|
| 807 |
+
<div class="dev-tag">dev:vessel-callsign</div>
|
| 808 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Call Sign</label>
|
| 809 |
<input type="text" id="vessel-callsign" style="padding:8px; width:100%;">
|
| 810 |
</div>
|
| 811 |
<div>
|
| 812 |
+
<div class="dev-tag">dev:vessel-mmsi</div>
|
| 813 |
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">MMSI Number</label>
|
| 814 |
<input type="text" id="vessel-mmsi" style="padding:8px; width:100%;">
|
| 815 |
</div>
|
| 816 |
+
<div>
|
| 817 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Vessel Hull Number</label>
|
| 818 |
+
<input type="text" id="vessel-hull-number" style="padding:8px; width:100%;">
|
| 819 |
+
</div>
|
| 820 |
+
<div>
|
| 821 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Starboard Engine Brand/Model/Size</label>
|
| 822 |
+
<input type="text" id="vessel-starboard-engine" style="padding:8px; width:100%;">
|
| 823 |
+
</div>
|
| 824 |
+
<div>
|
| 825 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Starboard Engine S/N</label>
|
| 826 |
+
<input type="text" id="vessel-starboard-sn" style="padding:8px; width:100%;">
|
| 827 |
+
</div>
|
| 828 |
+
<div>
|
| 829 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Port Engine Brand/Model/Size</label>
|
| 830 |
+
<input type="text" id="vessel-port-engine" style="padding:8px; width:100%;">
|
| 831 |
+
</div>
|
| 832 |
+
<div>
|
| 833 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">Port Engine S/N</label>
|
| 834 |
+
<input type="text" id="vessel-port-sn" style="padding:8px; width:100%;">
|
| 835 |
+
</div>
|
| 836 |
+
<div>
|
| 837 |
+
<label style="font-size:12px; font-weight:bold; margin-bottom:4px; display:block;">RIB S/N</label>
|
| 838 |
+
<input type="text" id="vessel-rib-sn" style="padding:8px; width:100%;">
|
| 839 |
+
</div>
|
| 840 |
</div>
|
| 841 |
+
<button onclick="saveVesselInfo()" class="btn btn-sm" style="background:var(--inquiry); width:100%;"><span class="dev-tag">dev:vessel-save</span>Save Vessel Info</button>
|
| 842 |
</div>
|
| 843 |
</div>
|
| 844 |
<div class="collapsible" style="margin-bottom:15px;">
|
|
|
|
| 853 |
<div class="col-body" style="padding:15px; background:#f8f9fa;">
|
| 854 |
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-add-fields</div>
|
| 855 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 856 |
+
<div><div class="dev-tag">dev:crew-add-first</div><label style="font-size:13px; margin-bottom:2px;">First Name *</label><input type="text" id="cn-first" style="padding:6px; width:100%;"></div>
|
| 857 |
+
<div><div class="dev-tag">dev:crew-add-middle</div><label style="font-size:13px; margin-bottom:2px;">Middle Name(s)</label><input type="text" id="cn-middle" style="padding:6px; width:100%;"></div>
|
| 858 |
+
<div><div class="dev-tag">dev:crew-add-last</div><label style="font-size:13px; margin-bottom:2px;">Last Name *</label><input type="text" id="cn-last" style="padding:6px; width:100%;"></div>
|
| 859 |
</div>
|
| 860 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 861 |
+
<div><div class="dev-tag">dev:crew-add-sex</div><label style="font-size:13px; margin-bottom:2px;">Sex</label><select id="cn-sex" style="padding:6px; width:100%;">
|
| 862 |
<option value="">Select...</option>
|
| 863 |
<option value="Male">Male</option>
|
| 864 |
<option value="Female">Female</option>
|
|
|
|
| 866 |
<option value="Other">Other</option>
|
| 867 |
<option value="Prefer not to say">Prefer not to say</option>
|
| 868 |
</select></div>
|
| 869 |
+
<div><div class="dev-tag">dev:crew-add-birthdate</div><label style="font-size:13px; margin-bottom:2px;">Birthdate</label><input type="date" id="cn-birthdate" style="padding:6px; width:100%;"></div>
|
| 870 |
+
<div><div class="dev-tag">dev:crew-add-position</div><label style="font-size:13px; margin-bottom:2px;">Position</label><select id="cn-position" style="padding:6px; width:100%;">
|
| 871 |
<option value="">Select...</option>
|
| 872 |
<option value="Captain">Captain</option>
|
| 873 |
<option value="Crew">Crew</option>
|
|
|
|
| 875 |
</select></div>
|
| 876 |
</div>
|
| 877 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 878 |
+
<div><div class="dev-tag">dev:crew-add-citizenship</div><label style="font-size:13px; margin-bottom:2px;">Citizenship</label><input type="text" id="cn-citizenship" list="countries" style="padding:6px; width:100%;"></div>
|
| 879 |
+
<div><div class="dev-tag">dev:crew-add-birthplace</div><label style="font-size:13px; margin-bottom:2px;">Birthplace</label><input type="text" id="cn-birthplace" list="countries" style="padding:6px; width:100%;"></div>
|
| 880 |
+
<div><div class="dev-tag">dev:crew-add-passport</div><label style="font-size:13px; margin-bottom:2px;">Passport Number</label><input type="text" id="cn-passport" style="padding:6px; width:100%;"></div>
|
| 881 |
+
<div><div class="dev-tag">dev:crew-add-pass-issue</div><label style="font-size:13px; margin-bottom:2px;">Issue Date</label><input type="date" id="cn-pass-issue" style="padding:6px; width:100%;"></div>
|
| 882 |
+
<div><div class="dev-tag">dev:crew-add-pass-expiry</div><label style="font-size:13px; margin-bottom:2px;">Expiry Date</label><input type="date" id="cn-pass-expiry" style="padding:6px; width:100%;"></div>
|
| 883 |
</div>
|
| 884 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 885 |
+
<div><div class="dev-tag">dev:crew-add-phone</div><label style="font-size:13px; margin-bottom:2px;">Cell/WhatsApp</label><input type="text" id="cn-phone" placeholder="+1234567890" style="padding:6px; width:100%;"></div>
|
| 886 |
+
<div><div class="dev-tag">dev:crew-add-passport-photo</div><label style="font-size:13px; margin-bottom:2px;">Passport Photo/PDF</label><input type="file" id="cn-passport-photo" accept="image/*,.pdf" style="padding:4px; width:100%; font-size:13px;"></div>
|
| 887 |
+
<div><div class="dev-tag">dev:crew-add-passport-page</div><label style="font-size:13px; margin-bottom:2px;">Passport Page Photo/PDF</label><input type="file" id="cn-passport-page" accept="image/*,.pdf" style="padding:4px; width:100%; font-size:13px;"></div>
|
| 888 |
</div>
|
| 889 |
<div style="margin-bottom:8px;"><label style="font-size:12px; font-weight:bold;">Emergency Contact</label></div>
|
| 890 |
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-add-emergency</div>
|
| 891 |
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:15px;">
|
| 892 |
+
<div><div class="dev-tag">dev:crew-add-emerg-name</div><label style="font-size:13px; margin-bottom:2px;">Name</label><input type="text" id="cn-emerg-name" style="padding:6px; width:100%;"></div>
|
| 893 |
+
<div><div class="dev-tag">dev:crew-add-emerg-rel</div><label style="font-size:13px; margin-bottom:2px;">Relationship</label><input type="text" id="cn-emerg-rel" style="padding:6px; width:100%;"></div>
|
| 894 |
+
<div><div class="dev-tag">dev:crew-add-emerg-phone</div><label style="font-size:13px; margin-bottom:2px;">Phone</label><input type="text" id="cn-emerg-phone" style="padding:6px; width:100%;"></div>
|
| 895 |
+
<div><div class="dev-tag">dev:crew-add-emerg-email</div><label style="font-size:13px; margin-bottom:2px;">Email</label><input type="email" id="cn-emerg-email" style="padding:6px; width:100%;"></div>
|
| 896 |
</div>
|
| 897 |
<div style="margin-bottom:10px; font-size:15px;">
|
| 898 |
<label style="font-size:13px; margin-bottom:2px;">Emergency Contact Notes</label>
|
| 899 |
+
<div class="dev-tag">dev:crew-add-emerg-notes</div>
|
| 900 |
<input type="text" id="cn-emerg-notes" placeholder="Additional emergency contact information" style="padding:6px; width:100%;">
|
| 901 |
</div>
|
| 902 |
<datalist id="countries">
|
| 903 |
<option value="USA"><option value="Canada"><option value="UK"><option value="Australia"><option value="New Zealand"><option value="France"><option value="Germany"><option value="Spain"><option value="Italy"><option value="Netherlands"><option value="Singapore"><option value="Malaysia"><option value="Thailand"><option value="Philippines"><option value="Japan"><option value="China"><option value="India">
|
| 904 |
</datalist>
|
| 905 |
+
<button onclick="addCrew()" class="btn btn-sm" style="background:var(--dark); width:100%;"><span class="dev-tag">dev:crew-add-submit</span>+ Add Crew Member</button>
|
| 906 |
</div>
|
| 907 |
</div>
|
| 908 |
|
| 909 |
+
<div class="dev-tag" style="margin-bottom:6px;">dev:crew-info-anchor</div>
|
| 910 |
<div id="crew-info-list"></div>
|
| 911 |
</div>
|
| 912 |
</div>
|
|
|
|
| 955 |
<div style="font-size:12px; color:#555;">Ensures all required models are cached and lets you back up/restore the cache before going offline.</div>
|
| 956 |
</div>
|
| 957 |
</div>
|
| 958 |
+
<div class="collapsible" style="margin-bottom:12px;">
|
| 959 |
+
<div class="col-header crew-med-header" onclick="toggleSection(this)" style="background:#fff; justify-content:flex-start;">
|
| 960 |
+
<span class="dev-tag">dev:settings-vax-types</span><span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Vaccine Type Dropdown</span>
|
| 961 |
+
</div>
|
| 962 |
+
<div class="col-body" style="padding:12px; background:#f8f9fa; display:none;">
|
| 963 |
+
<div style="margin-bottom:8px; font-size:12px; color:#2c3e50;">Manage the Vaccine Type/Disease options shown in Crew Vaccines. An "Other" option is always available for ad-hoc entries.</div>
|
| 964 |
+
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:10px;">
|
| 965 |
+
<input id="vaccine-type-input" type="text" style="padding:8px; min-width:220px; flex:1;" placeholder="e.g., MMR, DTaP, HepB">
|
| 966 |
+
<button class="btn btn-sm" style="background:var(--inquiry);" onclick="addVaccineType()">Add Type</button>
|
| 967 |
+
</div>
|
| 968 |
+
<div class="dev-tag" style="margin-bottom:6px;">dev:settings-vax-list</div>
|
| 969 |
+
<div id="vaccine-types-list"></div>
|
| 970 |
+
<div id="vaccine-types-status" style="margin-top:8px; font-size:12px; color:#555;">Saved with workspace settings.</div>
|
| 971 |
+
</div>
|
| 972 |
+
</div>
|
| 973 |
<div class="collapsible developer-only" style="margin-bottom:12px; display:none;">
|
| 974 |
<div class="col-header crew-med-header" onclick="toggleSection(this)" style="background:#fff; justify-content:flex-start;">
|
| 975 |
<span class="dev-tag">dev:settings-default-export</span><span class="detail-icon history-arrow" style="font-size:18px; margin-right:8px;">▸</span><span style="font-weight:700;">Export Default Dataset (Dev)</span>
|
|
|
|
| 1138 |
</div>
|
| 1139 |
</div>
|
| 1140 |
|
| 1141 |
+
<div id="chat-blocker">
|
| 1142 |
+
<div class="modal">
|
| 1143 |
+
<div class="spinner"></div>
|
| 1144 |
+
<h3>Processing Triage Chat…</h3>
|
| 1145 |
+
<p>Please wait. Navigation or edits in SailingMedAdvisor are temporarily paused until this response completes.</p>
|
| 1146 |
+
</div>
|
| 1147 |
+
</div>
|
| 1148 |
+
|
| 1149 |
<script src="/static/js/chat.js?v=2017"></script>
|
| 1150 |
+
<script src="/static/js/crew.js?v=2025"></script>
|
| 1151 |
<script src="/static/js/pharmacy.js?v=2026"></script>
|
| 1152 |
<script src="/static/js/equipment.js?v=2038"></script>
|
| 1153 |
+
<script src="/static/js/settings.js?v=2026"></script>
|
| 1154 |
+
<script src="/static/js/main.js?v=2020"></script>
|
| 1155 |
</body>
|
| 1156 |
</html>
|
templates/sidebars/sidebar_chat.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
<div class="sidebar-section">
|
| 3 |
<div class="sidebar-title">Overview</div>
|
| 4 |
<div class="sidebar-body">
|
| 5 |
-
This app was created out of the need for high quality medical guidance when there are no other options. Our family of six is circumnavigating on a sailboat. We are often in remote and inaccessible locations where there simply are no doctors.<br><br>
|
| 6 |
For example, we were anchored in a remote part of the Solomon Islands when our oldest son got a large three-prong off-shore fish hook stuck in his left cheek very close to his eye. It had large barbs and was impossible to pull out. Another time, our youngest son was biten by a dog suspected by the locals of having rabies. We needed to quickly cobble together a precautionary treatment from our medical chest. Another time, our son experienced a Shallow Water Blackout at a depth of 10m. He floated to the surface unconscious and was awakened by his younger siblings who were with him.<br><br>
|
| 7 |
In all cases, we needed timely access to medical assistance to address the situation. We struggled to find it, and hence, this app was born.</div>
|
| 8 |
</div>
|
|
|
|
| 2 |
<div class="sidebar-section">
|
| 3 |
<div class="sidebar-title">Overview</div>
|
| 4 |
<div class="sidebar-body">
|
| 5 |
+
This app was created out of the need for high quality medical guidance when there are no other options. Our family of six is circumnavigating on a sailboat. We are often in remote and inaccessible locations where there simply are no doctors to assist with a medical emergency.<br><br>
|
| 6 |
For example, we were anchored in a remote part of the Solomon Islands when our oldest son got a large three-prong off-shore fish hook stuck in his left cheek very close to his eye. It had large barbs and was impossible to pull out. Another time, our youngest son was biten by a dog suspected by the locals of having rabies. We needed to quickly cobble together a precautionary treatment from our medical chest. Another time, our son experienced a Shallow Water Blackout at a depth of 10m. He floated to the surface unconscious and was awakened by his younger siblings who were with him.<br><br>
|
| 7 |
In all cases, we needed timely access to medical assistance to address the situation. We struggled to find it, and hence, this app was born.</div>
|
| 8 |
</div>
|
templates/sidebars/sidebar_crew_medical.html
CHANGED
|
@@ -1,7 +1,18 @@
|
|
| 1 |
<div class="dev-tag">dev:sidebar-crew-medical</div>
|
| 2 |
<div class="sidebar-section" data-sidebar-section="crew-medical-section">
|
| 3 |
<div class="sidebar-title"><span class="sidebar-pill">Health Records</span> Intake Notes</div>
|
| 4 |
-
<div class="sidebar-body">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
</div>
|
| 6 |
<div class="sidebar-section">
|
| 7 |
<div class="sidebar-title">Updates & Exports</div>
|
|
|
|
| 1 |
<div class="dev-tag">dev:sidebar-crew-medical</div>
|
| 2 |
<div class="sidebar-section" data-sidebar-section="crew-medical-section">
|
| 3 |
<div class="sidebar-title"><span class="sidebar-pill">Health Records</span> Intake Notes</div>
|
| 4 |
+
<div class="sidebar-body">
|
| 5 |
+
When Logging is on the details of Triage and Inquiry chats
|
| 6 |
+
are saved here under each crew member's name. If Logging
|
| 7 |
+
is off then the chats are not saved.<br><br>
|
| 8 |
+
When you reactivate a past chat that chat is loaded into
|
| 9 |
+
the Triage or Inquiry section and you can continue from where
|
| 10 |
+
you last left-off. This is useful when you want to resubmit
|
| 11 |
+
a chat for further consideration by the larger model.<br><br>
|
| 12 |
+
When a crew member has not been selected for a chat the chat
|
| 13 |
+
will be logged under Unnamed Crew as either Triage chat or
|
| 14 |
+
an Inquiry chat.
|
| 15 |
+
</div>
|
| 16 |
</div>
|
| 17 |
<div class="sidebar-section">
|
| 18 |
<div class="sidebar-title">Updates & Exports</div>
|