mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
add clinic-console live-artifact template (#795)
* add clinic-console live-artifact template
Adds a built-in clinic-console template under
skills/live-artifact/assets/templates/, fulfilling the
assets/templates/<name> directory shape that
specs/2026-04-29-live-artifacts/spec.md §5.1 already plans for but
has not yet populated.
Three files, no other changes:
- template.html — html_template_v1 source, only DOM, CSS tokens, and {{data.*}} bindings
- data.json — canonical default sample (~6.7 KB, well within bounded JSON limits)
- README.md — data contract + telemedicine/pharmacy/pediatric variants
Verified locally against apps/daemon/src/live-artifacts/render.ts:
all paths match the html_template_v1 grammar, all bindings resolve
to scalars, no script/iframe/srcdoc/on*=/javascript:/raw HTML
directives, no forbidden JSON keys, JSON within bounded envelope.
* clinic-console: hardcode icon refs, drop icon_href bindings
Address @mrcfps's review on PR #795: stop interpolating {{data.*}} into
<use href="..."> attributes. The html_template_v1 binding contract
forbids interpolation inside URL-bearing attributes, and the security
validator runs *before* {{data.*}} substitution — so even a benign
icon_href today could be replaced with javascript:alert(1) tomorrow
without the validator ever seeing it.
Fix:
- template.html: every <use href="..."> is now a hardcoded literal
(#icon-dashboard, #icon-message, …). 14 nav slots + 4 KPI tiles
converted; total <use href="..."> count unchanged at 28, all
fragment-id literals, zero {{data.*}} inside URL-bearing attrs.
- data.json: icon_href removed from each nav_main[] item, each
nav_management[] item, and from kpi_a / kpi_b / kpi_c / kpi_d
(14 keys removed). Sample is now 6,333 bytes (was ~6,840).
- README.md: data-contract tables no longer list icon_href; new
"Icons are template-locked" section explains the binding-contract
rationale, lists the hardcoded id per slot, and gives guidance for
future runtime-configurable icons (route through a constrained,
non-URL mechanism such as enumerated CSS classes — never interpolate
into <use href> directly).
Verified locally against apps/daemon/src/live-artifacts/render.ts:
298 bindings, 297 unique paths, all resolve to scalars; zero
{{...}} inside any URL-bearing attribute (href / src / action /
formaction / srcset / xlink:href / ping / background / poster /
cite); none of the 6 forbidden security patterns present; bounded
JSON envelope: 6.3 KiB / depth 5 / max 22 keys / max 35 array items
/ max 45-char string — all well within limits.
---------
Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
This commit is contained in:
parent
55aa24167b
commit
2bbd677ea2
3 changed files with 1055 additions and 0 deletions
342
skills/live-artifact/assets/templates/clinic-console/README.md
Normal file
342
skills/live-artifact/assets/templates/clinic-console/README.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# Clinic Console — live artifact template
|
||||
|
||||
A `html_template_v1` template for a friendly clinic / hospital / telemedicine
|
||||
operations console. Soft-mint healthcare aesthetic: cool off-white canvas,
|
||||
single mint accent, generous 18px card radii, signature diagonal-stripe
|
||||
pattern fills inside KPI tiles and bar-chart bars, illustrated CSS-gradient
|
||||
avatars, and one dark surface (the calendar activity popover).
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
clinic-console/
|
||||
├── template.html # html_template_v1 source — only DOM, CSS tokens, and {{data.*}} bindings
|
||||
├── data.json # canonical default sample (renders straight out of the box)
|
||||
└── README.md # this file — data contract + customization notes
|
||||
```
|
||||
|
||||
## How an agent uses this template
|
||||
|
||||
This template is intended to be copied (or referenced) when the
|
||||
[`live-artifact`](../../../SKILL.md) skill is invoked with a healthcare
|
||||
operations brief such as *"clinic dashboard", "doctors schedule", "hospital
|
||||
admin", "appointment console", "telemedicine ops", "诊所后台", "医院管理"*.
|
||||
|
||||
The agent should:
|
||||
|
||||
1. Copy `template.html` and `data.json` into the project's live-artifact
|
||||
workspace directory as `template.html` and `data.json`.
|
||||
2. Edit `data.json` to reflect the actual brand, names, schedules, and
|
||||
numbers from the brief or connector source. The shape of the JSON must
|
||||
match what `template.html` references (every `{{data.path}}` interpolation
|
||||
below).
|
||||
3. Author `artifact.json` and `provenance.json` per the live-artifact
|
||||
protocol, then register the artifact through the daemon wrapper:
|
||||
|
||||
```bash
|
||||
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json
|
||||
```
|
||||
|
||||
4. The daemon renders `template.html + data.json` into the preview
|
||||
`index.html` automatically. The agent does **not** author `index.html`.
|
||||
|
||||
When the user clicks **Refresh**, the daemon re-runs the registered source,
|
||||
maps results back into `data.json`, re-renders the preview, and snapshots
|
||||
the change — the layout never changes; only the numbers, names, and pill
|
||||
states do.
|
||||
|
||||
## Default sample renders out of the box
|
||||
|
||||
If you create a live artifact using the default `data.json` shipped here,
|
||||
you get the canonical "St. Lukes Wellness" demo screen:
|
||||
|
||||
- Greeting: `Hey Lukmon, glad to have you back! 🙌`
|
||||
- Four KPI tiles: Total doctors / Total bookings / Available rooms / Total
|
||||
visitors, with mixed amber- and blue-stripe pattern footers and an inline
|
||||
General/Private rooms mini-list.
|
||||
- Patient overview chart with paired diagonal-stripe bars across Jan–Jul,
|
||||
Mar 2025 highlighted with a mint outline.
|
||||
- March 2025 mini-calendar with day 8 active (mint circle + dot) and a dark
|
||||
Activity Detail popover floating below it.
|
||||
- Top requested clinics donut: Dental 120 / Cardiology 249 / Surgery 165.
|
||||
- Doctor schedule with three pastel pills (Available / Unavailable / Leave).
|
||||
- Today's appointments list with five illustrated avatars + venue / mode
|
||||
hints (`room 204`, `video call`, …).
|
||||
|
||||
## Default sample provenance.json
|
||||
|
||||
If you ship the default sample without re-sourcing the data, use:
|
||||
|
||||
```json
|
||||
{
|
||||
"generatedAt": "2026-04-29T12:00:00.000Z",
|
||||
"generatedBy": "agent",
|
||||
"notes": "Default sample data shipped with the clinic-console template. Replace with real clinic data before sharing externally.",
|
||||
"sources": [
|
||||
{ "label": "Template default sample", "type": "user_input" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Data contract
|
||||
|
||||
The shape below is the contract between `template.html` and `data.json`.
|
||||
Every key listed is referenced by at least one `{{data.path}}` interpolation
|
||||
in `template.html`. All values are scalars (string or number); the template
|
||||
does not invoke any expression / helper / conditional logic — it is a
|
||||
straight `html_template_v1` substitution.
|
||||
|
||||
### Top-level scalars
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `brand_name` | `"ST. LUKES"` | Sidebar wordmark. Keep ≤14 characters. |
|
||||
| `greeting` | `"Hey Lukmon, glad to have you back! 🙌"` | Single emoji allowed at the end; no other emoji anywhere in the artifact. |
|
||||
| `search_placeholder` | `"Search doctors, patients, rooms…"` | Greeting-row search input ghost text. |
|
||||
| `search_shortcut` | `"⌘K"` | Right-side keycap label. |
|
||||
| `secondary_action_label` | `"Export CSV"` | Greeting-row secondary button text. |
|
||||
| `primary_action_label` | `"Add new"` | Greeting-row primary mint CTA text. |
|
||||
|
||||
### `user`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `name` | `"Lukmon Olabode"` | Sidebar bottom row. |
|
||||
| `role` | `"Admin"` | One-word role; longer roles wrap. |
|
||||
| `av_class` | `"av-orange"` | One of `av-orange`, `av-pink`, `av-mint`, `av-blue`, `av-violet`, `av-amber`, `av-rose`. |
|
||||
| `initial` | `"L"` | Single uppercase letter. |
|
||||
|
||||
### `nav_main` and `nav_management` (5 items each)
|
||||
|
||||
Each item shape:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `label` | `"Dashboard"` | Nav text. |
|
||||
| `active_class` | `""` or `"active"` | Set to `"active"` on exactly one nav item across both groups. |
|
||||
| `count` | `""` or `"10"` | Empty string hides the count badge (CSS `:empty { display: none }`). |
|
||||
|
||||
> **Icons are template-locked.** Each nav slot's icon is hardcoded inside
|
||||
> `template.html` (see [Icons are template-locked](#icons-are-template-locked)
|
||||
> below) and is not exposed through `data.json`. The `html_template_v1`
|
||||
> security validator forbids `{{data.*}}` interpolation inside URL-bearing
|
||||
> attributes (`<use href>`, `<a href>`, `<img src>`, …) — and even if it
|
||||
> didn't, the validator runs *before* substitution, so a malformed `data.json`
|
||||
> could smuggle a `javascript:` URL past it. The reorder rule is therefore:
|
||||
> if you change the meaning of a nav slot, also edit the corresponding
|
||||
> `<use href="#icon-…">` literal in `template.html`.
|
||||
|
||||
### `pro_card`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `tag` | `"Pro"` | Black pill in the upgrade card. Keep ≤6 characters. |
|
||||
| `title` | `"Pssst!"` | Display title. |
|
||||
| `body` | `"Your subscription expires in 9 days."` | One-sentence nudge. |
|
||||
| `primary_label` | `"Renew"` | Mint primary action. |
|
||||
| `secondary_label` | `"Cancel"` | Outlined secondary action. |
|
||||
|
||||
### KPI tiles `kpi_a` `kpi_b` `kpi_c` `kpi_d`
|
||||
|
||||
Tiles A, B, D share the **caption + pattern strip** layout. Tile C uses a
|
||||
**2-row mini-list** layout instead. Every tile must have either a strip or a
|
||||
mini-list — never bare.
|
||||
|
||||
Common keys:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `label` | `"Total doctors"` | Tile label. |
|
||||
| `value` | `"1,089"` | Big number (Plus Jakarta Sans 700). Use commas for thousands. |
|
||||
| `trend_class` | `"up"` or `"down"` | Pill grammar — `up` = mint, `down` = rose. |
|
||||
| `trend_label` | `"↑ 5.5%"` | Always include the arrow glyph. |
|
||||
|
||||
> KPI icons are also template-locked — see
|
||||
> [Icons are template-locked](#icons-are-template-locked) below.
|
||||
|
||||
A / B / D additional keys:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `caption` | `"An increase of 20 doctors in the last 7 days."` | One sentence answering "compared to what". |
|
||||
| `strip_class` | `"stripe-amber"` | One of `stripe-amber`, `stripe-blue`, `stripe-mint`. Adjacent tiles should alternate hues. |
|
||||
| `mini_stat` (B / D only) | `"1,635 today"` | Right-aligned tiny caption below the strip. |
|
||||
|
||||
C (`kpi_c`) additional keys:
|
||||
|
||||
| Key | Example |
|
||||
|---|---|
|
||||
| `rows` | array of 2 objects: `{ "label": "General room", "value": "100" }` |
|
||||
|
||||
### `chart`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `"Patient overview"` | Card title. |
|
||||
| `dropdown_label` | `"Last 6 months"` | Time-range chip text. |
|
||||
| `legend_a` `legend_b` `legend_c` | `"Total patients"` etc. | Three legend captions. |
|
||||
| `bars` | array of 14 objects: `{ "x": "34", "y": "148", "h": "92" }` | 7 month pairs (mint back, blue front). Bar 5 (index 5) is the highlighted month — the template adds a 2px mint stroke to bar 5 only. |
|
||||
| `x_labels` | `["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]` | Seven month labels matching the seven bar pairs. |
|
||||
|
||||
### `calendar`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `month_label` | `"March 2025"` | Header. |
|
||||
| `dow` | `["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]` | Always 7 items. |
|
||||
| `days` | array of exactly **35** objects: `{ "label": "1", "modifier": "" }` | 5 weeks × 7 days. `modifier` = `""`, `"muted"` (leading/trailing month), or `"active"` (single highlighted day, mint circle). |
|
||||
|
||||
### `activity`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `"Activity Detail · Mar 8"` | Popover header — should reference the active calendar day. |
|
||||
| `events` | array of exactly 3 objects: `{ "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" }` | Three events. `av_class` ∈ `blue`, `pink`, `violet`, `mint`, `amber`. |
|
||||
| `add_label` | `"+ Add item"` | Footer link. |
|
||||
|
||||
### `donut`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `"Top 3 most requested clinics"` | Card title. |
|
||||
| `center_label` | `"Total patients"` | Above the center number. |
|
||||
| `center_num` | `"534"` | The big tabular number in the donut hole. |
|
||||
| `segment_b` | `{ "dasharray": "120 240", "dashoffset": "0" }` | SVG `stroke-dasharray` + `stroke-dashoffset` for the pink slice. Sum of arc lengths over a r=38 circle equals `2π × 38 ≈ 239`. |
|
||||
| `segment_c` | `{ "dasharray": "35 240", "dashoffset": "-120" }` | Same for the mint slice. The blue background ring is the full circumference — no per-segment math needed. |
|
||||
| `legend` | array of exactly 3 objects: `{ "value": "120", "label": "Dental" }` | Three entries in blue / pink / mint order. |
|
||||
|
||||
### `schedule`
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `"Doctors's schedule"` | Card title. |
|
||||
| `stats` | array of exactly 3 objects: `{ "value": "51", "small": "Total", "label": "Available" }` | Available / Unavailable / Leave counts in a 3-column grid. |
|
||||
| `list_header_label` | `"List of Doctor"` | Sortable list header label. |
|
||||
| `doctors` | array of exactly 4 objects | See below. |
|
||||
|
||||
Each `doctors` row:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `av_class` | `"av-blue"` | Avatar gradient. |
|
||||
| `initial` | `"P"` | Single uppercase letter. |
|
||||
| `name` | `"Peter Bashir"` | Real-feeling name. |
|
||||
| `role` | `"Anesthesiologist"` | Specialty. |
|
||||
| `status_class` | `"avail"` `"unav"` `"leave"` | Pill grammar. |
|
||||
| `status_label` | `"Available"` `"Unavailable"` `"Leave"` | Pill text. |
|
||||
|
||||
### `appointments`
|
||||
|
||||
| Key | Example |
|
||||
|---|---|
|
||||
| `title` | `"Today's appointments"` |
|
||||
| `list` | array of exactly 5 objects |
|
||||
|
||||
Each `list` row:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|---|---|---|
|
||||
| `av_class` | `"av-pink"` | Avatar gradient. |
|
||||
| `initial` | `"R"` | Single uppercase letter. |
|
||||
| `name` | `"Ruth Tubonimi"` | Real-feeling name. |
|
||||
| `role` | `"Gastroenterology · room 204"` | Specialty + venue / mode hint (`room N`, `video call`, `telemedicine`). |
|
||||
| `date` | `"Today"` | Short date label. |
|
||||
| `time` | `"09:40"` | 24h or 12h, pick one and stay consistent. |
|
||||
|
||||
## Icons are template-locked
|
||||
|
||||
`html_template_v1` forbids `{{data.*}}` interpolation inside URL-bearing
|
||||
attributes such as `<use href>`, `<a href>`, `<img src>`,
|
||||
`<form action>`, etc. (see
|
||||
[`skills/live-artifact/references/artifact-schema.md`](../../../references/artifact-schema.md#html-template-v1-binding-rules)).
|
||||
The renderer's security validator runs *before* `{{data.*}}` substitution, so
|
||||
even a well-formed validator pass would not protect a future `data.json`
|
||||
that put `javascript:alert(1)` (or any other URL value) into one of these
|
||||
attributes.
|
||||
|
||||
This template therefore hardcodes every `<use href="#icon-…">` reference in
|
||||
`template.html` itself. Each slot has a fixed icon id:
|
||||
|
||||
| Slot | Hardcoded icon id |
|
||||
|---|---|
|
||||
| Sidebar brand mark | `#icon-leaf` |
|
||||
| Sidebar collapse toggle | `#icon-collapse` |
|
||||
| `nav_main[0]` Dashboard | `#icon-dashboard` |
|
||||
| `nav_main[1]` Message | `#icon-message` |
|
||||
| `nav_main[2]` Schedule | `#icon-schedule` |
|
||||
| `nav_main[3]` Notification | `#icon-bell` |
|
||||
| `nav_main[4]` Transaction | `#icon-card` |
|
||||
| `nav_management[0]` Doctor | `#icon-user` |
|
||||
| `nav_management[1]` Medicine | `#icon-pill` |
|
||||
| `nav_management[2]` Bedroom | `#icon-bed` |
|
||||
| `nav_management[3]` Appointment | `#icon-check-square` |
|
||||
| `nav_management[4]` Patient | `#icon-people` |
|
||||
| Sidebar logout | `#icon-logout` |
|
||||
| Greeting-row search | `#icon-search` |
|
||||
| Greeting-row secondary CTA | `#icon-download` |
|
||||
| Greeting-row primary CTA | `#icon-plus` |
|
||||
| `kpi_a` glyph | `#icon-user` |
|
||||
| `kpi_b` glyph | `#icon-schedule` |
|
||||
| `kpi_c` glyph | `#icon-bed` |
|
||||
| `kpi_d` glyph | `#icon-people` |
|
||||
| Patient-overview card | `#icon-clock` |
|
||||
| Time-range dropdown chevron | `#icon-chev-down` |
|
||||
| Calendar prev / next | `#icon-chev-left`, `#icon-chev-right` |
|
||||
| Top-clinics card | `#icon-stethoscope` |
|
||||
| Doctor-schedule card | `#icon-schedule` |
|
||||
| List header chevron | `#icon-chev-down` |
|
||||
| Today's-appointments card | `#icon-check-square` |
|
||||
|
||||
If you re-purpose a slot (e.g. swap `nav_main[2] Schedule` for
|
||||
`nav_main[2] Reports`), edit the corresponding `<use href="#icon-…">` literal
|
||||
in `template.html` to match — the icon set inside the inline `<symbol>`
|
||||
defs at the top of `template.html` already includes 21 icons covering the
|
||||
common clinic / hospital / pharmacy / telemedicine vocabulary
|
||||
(`#icon-dashboard`, `#icon-message`, `#icon-schedule`, `#icon-bell`,
|
||||
`#icon-card`, `#icon-user`, `#icon-pill`, `#icon-bed`, `#icon-check-square`,
|
||||
`#icon-people`, `#icon-leaf`, `#icon-clock`, `#icon-stethoscope`,
|
||||
`#icon-search`, `#icon-download`, `#icon-plus`, `#icon-chev-left`,
|
||||
`#icon-chev-right`, `#icon-chev-down`, `#icon-collapse`, `#icon-logout`).
|
||||
|
||||
If you need a runtime-configurable icon, add a new constrained,
|
||||
non-URL-bearing mechanism (for example a `data.kpi_a.icon_class` that toggles
|
||||
between a fixed list of CSS classes the template enumerates) — never
|
||||
interpolate into `<use href>` directly.
|
||||
|
||||
## Style guarantees
|
||||
|
||||
The template enforces, in CSS only (no JavaScript):
|
||||
|
||||
- Cool off-white canvas (`#EEF2F6`), bright white surfaces, 18px card radii, 1px hairline borders.
|
||||
- Mint accent (`#10B981`) restricted to five places: active sidebar nav row, primary CTA, KPI icon glyphs, success metric pill, active calendar date.
|
||||
- Diagonal-stripe pattern fills (135°, 8px line + 8px gap) on KPI footer strips and inside bar-chart bars.
|
||||
- Pastel-only status pills (mint / rose / amber).
|
||||
- Tabular lining numerals on every numeric value (`font-feature-settings: "tnum","lnum"`).
|
||||
- The dark calendar activity popover is the only dark surface in the artifact.
|
||||
- Mobile reflow at ≤920px: sidebar stacks above main, KPI strip becomes 2 cols then 1 col, mid and bottom rows stack.
|
||||
- No external CDN imports. Fonts use system fallback (`Plus Jakarta Sans, Inter, system-ui, sans-serif`).
|
||||
|
||||
## Customization tips
|
||||
|
||||
- **Telemedicine** variant: replace `kpi_c` (Available rooms) with `Live sessions`, swap the donut to `Top consultation types` (Video / Audio / Chat), and add `· video call` / `· audio call` venue hints in appointment rows.
|
||||
- **Pharmacy** variant: replace the doctor schedule with stock levels — keep the same shape, just rename the columns to SKU / drug / stock pill.
|
||||
- **Pediatric** variant: tilt the avatar palette toward `av-pink`, `av-amber`, `av-orange`, keep the active calendar day on a children's milestone.
|
||||
|
||||
For all variants, **do not** introduce new colors, fonts, or radii. Every visual lever is already a token in `:root{}`.
|
||||
|
||||
## Bounded JSON envelope
|
||||
|
||||
This default `data.json` is well within the live-artifact bounded JSON
|
||||
constraints:
|
||||
|
||||
| Constraint | Limit | This sample |
|
||||
|---|---|---|
|
||||
| Object/array depth | 8 | 4 |
|
||||
| Object keys | 100 / object | ≤20 |
|
||||
| Array length | 500 | 35 (calendar.days) |
|
||||
| String length | 16 KiB | <100 chars |
|
||||
| Serialized size | 256 KiB | ~7 KiB |
|
||||
|
||||
If you scale up the bar count, calendar density, or list rows, stay well
|
||||
under these limits. Refresh writes go through the same validation, so
|
||||
oversized data will be rejected before persistence.
|
||||
165
skills/live-artifact/assets/templates/clinic-console/data.json
Normal file
165
skills/live-artifact/assets/templates/clinic-console/data.json
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"brand_name": "ST. LUKES",
|
||||
"greeting": "Hey Lukmon, glad to have you back! 🙌",
|
||||
"search_placeholder": "Search doctors, patients, rooms…",
|
||||
"search_shortcut": "⌘K",
|
||||
"secondary_action_label": "Export CSV",
|
||||
"primary_action_label": "Add new",
|
||||
|
||||
"user": {
|
||||
"name": "Lukmon Olabode",
|
||||
"role": "Admin",
|
||||
"av_class": "av-orange",
|
||||
"initial": "L"
|
||||
},
|
||||
|
||||
"nav_main_label": "Main Menu",
|
||||
"nav_main": [
|
||||
{ "label": "Dashboard", "active_class": "active", "count": "" },
|
||||
{ "label": "Message", "active_class": "", "count": "10" },
|
||||
{ "label": "Schedule", "active_class": "", "count": "" },
|
||||
{ "label": "Notification", "active_class": "", "count": "12" },
|
||||
{ "label": "Transaction", "active_class": "", "count": "" }
|
||||
],
|
||||
|
||||
"nav_management_label": "Management",
|
||||
"nav_management": [
|
||||
{ "label": "Doctor", "active_class": "", "count": "" },
|
||||
{ "label": "Medicine", "active_class": "", "count": "" },
|
||||
{ "label": "Bedroom", "active_class": "", "count": "" },
|
||||
{ "label": "Appointment", "active_class": "", "count": "" },
|
||||
{ "label": "Patient", "active_class": "", "count": "" }
|
||||
],
|
||||
|
||||
"pro_card": {
|
||||
"tag": "Pro",
|
||||
"title": "Pssst!",
|
||||
"body": "Your subscription expires in 9 days.",
|
||||
"primary_label": "Renew",
|
||||
"secondary_label": "Cancel"
|
||||
},
|
||||
|
||||
"kpi_a": {
|
||||
"label": "Total doctors",
|
||||
"value": "1,089",
|
||||
"trend_class": "down",
|
||||
"trend_label": "↓ 4.2%",
|
||||
"caption": "An increase of 20 doctors in the last 7 days.",
|
||||
"strip_class": "stripe-amber"
|
||||
},
|
||||
"kpi_b": {
|
||||
"label": "Total bookings",
|
||||
"value": "17,610",
|
||||
"trend_class": "up",
|
||||
"trend_label": "↑ 5.5%",
|
||||
"caption": "Last 7 days: 5,231 → 8,323 visitors.",
|
||||
"strip_class": "stripe-blue",
|
||||
"mini_stat": "1,635 today"
|
||||
},
|
||||
"kpi_c": {
|
||||
"label": "Available rooms",
|
||||
"value": "8,450",
|
||||
"trend_class": "up",
|
||||
"trend_label": "↑ 462",
|
||||
"rows": [
|
||||
{ "label": "General room", "value": "100" },
|
||||
{ "label": "Private room", "value": "75" }
|
||||
]
|
||||
},
|
||||
"kpi_d": {
|
||||
"label": "Total visitors",
|
||||
"value": "29,709",
|
||||
"trend_class": "up",
|
||||
"trend_label": "↑ 3.5%",
|
||||
"caption": "Top 3 in-demand clinics this month.",
|
||||
"strip_class": "stripe-amber",
|
||||
"mini_stat": "1,070 today"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
"title": "Patient overview",
|
||||
"dropdown_label": "Last 6 months",
|
||||
"legend_a": "Total patients",
|
||||
"legend_b": "Avg. hospitalized",
|
||||
"legend_c": "Avg. outpatient care",
|
||||
"bars": [
|
||||
{ "x": "34", "y": "148", "h": "92" },
|
||||
{ "x": "58", "y": "108", "h": "132" },
|
||||
{ "x": "120", "y": "124", "h": "116" },
|
||||
{ "x": "144", "y": "92", "h": "148" },
|
||||
{ "x": "206", "y": "96", "h": "144" },
|
||||
{ "x": "230", "y": "56", "h": "184" },
|
||||
{ "x": "292", "y": "156", "h": "84" },
|
||||
{ "x": "316", "y": "116", "h": "124" },
|
||||
{ "x": "378", "y": "140", "h": "100" },
|
||||
{ "x": "402", "y": "100", "h": "140" },
|
||||
{ "x": "464", "y": "120", "h": "120" },
|
||||
{ "x": "488", "y": "80", "h": "160" },
|
||||
{ "x": "550", "y": "160", "h": "80" },
|
||||
{ "x": "574", "y": "128", "h": "112" }
|
||||
],
|
||||
"x_labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"month_label": "March 2025",
|
||||
"dow": ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"],
|
||||
"days": [
|
||||
{ "label": "23", "modifier": "muted" }, { "label": "24", "modifier": "muted" }, { "label": "25", "modifier": "muted" }, { "label": "26", "modifier": "muted" }, { "label": "27", "modifier": "muted" }, { "label": "28", "modifier": "muted" }, { "label": "1", "modifier": "" },
|
||||
{ "label": "2", "modifier": "" }, { "label": "3", "modifier": "" }, { "label": "4", "modifier": "" }, { "label": "5", "modifier": "" }, { "label": "6", "modifier": "" }, { "label": "7", "modifier": "" }, { "label": "8", "modifier": "active" },
|
||||
{ "label": "9", "modifier": "" }, { "label": "10", "modifier": "" }, { "label": "11", "modifier": "" }, { "label": "12", "modifier": "" }, { "label": "13", "modifier": "" }, { "label": "14", "modifier": "" }, { "label": "15", "modifier": "" },
|
||||
{ "label": "16", "modifier": "" }, { "label": "17", "modifier": "" }, { "label": "18", "modifier": "" }, { "label": "19", "modifier": "" }, { "label": "20", "modifier": "" }, { "label": "21", "modifier": "" }, { "label": "22", "modifier": "" },
|
||||
{ "label": "23", "modifier": "" }, { "label": "24", "modifier": "" }, { "label": "25", "modifier": "" }, { "label": "26", "modifier": "" }, { "label": "27", "modifier": "" }, { "label": "28", "modifier": "" }, { "label": "29", "modifier": "" }
|
||||
]
|
||||
},
|
||||
|
||||
"activity": {
|
||||
"title": "Activity Detail · Mar 8",
|
||||
"events": [
|
||||
{ "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" },
|
||||
{ "av_class": "pink", "name": "Dental staff meetup", "time": "3:00 pm" },
|
||||
{ "av_class": "violet", "name": "Ola Muhammad intake", "time": "4:00 pm" }
|
||||
],
|
||||
"add_label": "+ Add item"
|
||||
},
|
||||
|
||||
"donut": {
|
||||
"title": "Top 3 most requested clinics",
|
||||
"center_label": "Total patients",
|
||||
"center_num": "534",
|
||||
"segment_b": { "dasharray": "120 240", "dashoffset": "0" },
|
||||
"segment_c": { "dasharray": "35 240", "dashoffset": "-120" },
|
||||
"legend": [
|
||||
{ "value": "120", "label": "Dental" },
|
||||
{ "value": "249", "label": "Cardiology" },
|
||||
{ "value": "165", "label": "Surgery" }
|
||||
]
|
||||
},
|
||||
|
||||
"schedule": {
|
||||
"title": "Doctors's schedule",
|
||||
"stats": [
|
||||
{ "value": "51", "small": "Total", "label": "Available" },
|
||||
{ "value": "23", "small": "Total", "label": "Unavailable" },
|
||||
{ "value": "09", "small": "Total", "label": "Leave" }
|
||||
],
|
||||
"list_header_label": "List of Doctor",
|
||||
"doctors": [
|
||||
{ "av_class": "av-blue", "initial": "P", "name": "Peter Bashir", "role": "Anesthesiologist", "status_class": "avail", "status_label": "Available" },
|
||||
{ "av_class": "av-violet", "initial": "D", "name": "Deborah Fagbemi", "role": "Cardiologist", "status_class": "unav", "status_label": "Unavailable" },
|
||||
{ "av_class": "av-rose", "initial": "H", "name": "Hannah Diongoli", "role": "Dermatologist", "status_class": "avail", "status_label": "Available" },
|
||||
{ "av_class": "av-amber", "initial": "A", "name": "Aisha Bello", "role": "Pediatrician", "status_class": "leave", "status_label": "Leave" }
|
||||
]
|
||||
},
|
||||
|
||||
"appointments": {
|
||||
"title": "Today's appointments",
|
||||
"list": [
|
||||
{ "av_class": "av-pink", "initial": "R", "name": "Ruth Tubonimi", "role": "Gastroenterology · room 204", "date": "Today", "time": "09:40" },
|
||||
{ "av_class": "av-amber", "initial": "J", "name": "Joseph Obiano", "role": "Psychiatry · video call", "date": "Today", "time": "10:25" },
|
||||
{ "av_class": "av-mint", "initial": "T", "name": "Timothy Jibrin", "role": "Hematology · room 117", "date": "Today", "time": "11:00" },
|
||||
{ "av_class": "av-violet", "initial": "E", "name": "Elizabeth Kanu", "role": "Ophthalmology · room 09", "date": "Today", "time": "02:15" },
|
||||
{ "av_class": "av-blue", "initial": "S", "name": "Simon Garba", "role": "Otolaryngology · room 21", "date": "Today", "time": "03:40" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{data.brand_name}} — Clinic Operations Console</title>
|
||||
<style>
|
||||
:root {
|
||||
--canvas: #eef2f6;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f8fafc;
|
||||
--surface-inverse: #0f172a;
|
||||
--border: #e5e7eb;
|
||||
--border-strong: #d6dbe3;
|
||||
--fg: #0f172a;
|
||||
--fg-muted: #64748b;
|
||||
--fg-tertiary: #94a3b8;
|
||||
--fg-on-inverse: #f8fafc;
|
||||
--accent: #10b981;
|
||||
--accent-strong: #059669;
|
||||
--accent-soft: #d1fae5;
|
||||
--pill-up-bg: #d1fae5; --pill-up-fg: #059669;
|
||||
--pill-down-bg: #fee2e2; --pill-down-fg: #dc2626;
|
||||
--pill-avail-bg: #d1fae5; --pill-avail-fg: #047857;
|
||||
--pill-unav-bg: #fee2e2; --pill-unav-fg: #b91c1c;
|
||||
--pill-leave-bg: #fef3c7; --pill-leave-fg: #b45309;
|
||||
--stripe-blue: #bfdbfe; --stripe-amber: #fde68a; --stripe-mint: #a7f3d0;
|
||||
--donut-blue: #bfdbfe; --donut-pink: #fbcfe8; --donut-mint: #ccfbf1;
|
||||
--legend-blue: #3b82f6; --legend-amber: #f59e0b; --legend-pink: #ec4899; --legend-mint: #2dd4bf;
|
||||
--av-orange-1: #fed7aa; --av-orange-2: #f97316; --av-orange-fg: #7c2d12;
|
||||
--av-pink-1: #fbcfe8; --av-pink-2: #ec4899;
|
||||
--av-mint-1: #a7f3d0; --av-mint-2: #10b981;
|
||||
--av-blue-1: #bfdbfe; --av-blue-2: #3b82f6;
|
||||
--av-violet-1: #ddd6fe; --av-violet-2: #7c3aed;
|
||||
--av-amber-1: #fde68a; --av-amber-2: #f59e0b; --av-amber-fg: #78350f;
|
||||
--av-rose-1: #fecaca; --av-rose-2: #ef4444;
|
||||
--av-fg-light: #ffffff;
|
||||
--r-card: 18px; --r-pill: 999px; --r-md: 14px; --r-sm: 10px; --r-xs: 8px;
|
||||
--shadow-card: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.04);
|
||||
--shadow-popover: 0 12px 28px rgba(2,6,23,.32);
|
||||
--font-display: 'Plus Jakarta Sans', 'SF Pro Display', system-ui, sans-serif;
|
||||
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 20px;
|
||||
background: var(--canvas); color: var(--fg);
|
||||
font: 14px/1.5 var(--font-body);
|
||||
font-feature-settings: "tnum", "lnum";
|
||||
display: grid; grid-template-columns: 240px 1fr; gap: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
body { grid-template-columns: 1fr; }
|
||||
aside.sidebar { position: static !important; height: auto !important; }
|
||||
}
|
||||
.card, aside.sidebar {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: var(--r-card); box-shadow: var(--shadow-card);
|
||||
}
|
||||
aside.sidebar {
|
||||
padding: 18px; display: flex; flex-direction: column; gap: 16px;
|
||||
position: sticky; top: 20px; height: calc(100vh - 40px);
|
||||
}
|
||||
.brand { display: flex; align-items: center; justify-content: space-between; padding: 0 6px; }
|
||||
.brand-mark { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -0.01em; }
|
||||
.brand-leaf { width: 22px; height: 22px; border-radius: var(--r-pill); background: var(--accent); color: var(--surface); display: inline-flex; align-items: center; justify-content: center; }
|
||||
.brand-toggle { color: var(--fg-tertiary); }
|
||||
.nav-section { display: flex; flex-direction: column; gap: 2px; }
|
||||
.nav-label { color: var(--fg-tertiary); font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; padding: 8px 10px 4px; }
|
||||
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm); color: var(--fg-muted); font-size: 13px; }
|
||||
.nav-item .ico { color: var(--fg-muted); }
|
||||
.nav-item .count { margin-left: auto; background: var(--surface-muted); color: var(--fg-muted); border-radius: var(--r-pill); padding: 1px 8px; font-size: 11px; font-weight: 500; }
|
||||
.nav-item .count:empty { display: none; }
|
||||
.nav-item.active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }
|
||||
.nav-item.active .ico { color: var(--accent-strong); }
|
||||
.pro-card { margin-top: auto; background: var(--surface-muted); border-radius: var(--r-md); padding: 14px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.pro-tag { display: inline-flex; background: var(--fg); color: var(--surface); border-radius: var(--r-pill); padding: 2px 8px; font-size: 10px; font-weight: 600; width: fit-content; }
|
||||
.pro-card h4 { margin: 0; font-size: 13px; font-weight: 600; font-family: var(--font-display); }
|
||||
.pro-card p { margin: 0; font-size: 11px; color: var(--fg-muted); }
|
||||
.pro-actions { display: flex; gap: 8px; margin-top: 4px; }
|
||||
.pro-actions button { font-size: 11px; padding: 6px 12px; border-radius: var(--r-sm); border: 1px solid var(--border); background: var(--surface); color: var(--fg-muted); cursor: pointer; }
|
||||
.pro-actions .primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
|
||||
.user-row { display: flex; align-items: center; gap: 10px; padding: 10px 6px 0; border-top: 1px solid var(--border); }
|
||||
.user-row .meta { display: flex; flex-direction: column; }
|
||||
.user-row .name { font-size: 13px; font-weight: 600; }
|
||||
.user-row .role { font-size: 11px; color: var(--fg-tertiary); }
|
||||
.user-row .out { margin-left: auto; color: var(--fg-tertiary); }
|
||||
|
||||
.av { border-radius: var(--r-pill); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--av-fg-light); font-weight: 600; }
|
||||
.av-32 { width: 32px; height: 32px; font-size: 12px; }
|
||||
.av-40 { width: 40px; height: 40px; font-size: 14px; }
|
||||
.av-orange { background: linear-gradient(135deg, var(--av-orange-1), var(--av-orange-2)); color: var(--av-orange-fg); }
|
||||
.av-pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
|
||||
.av-mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
|
||||
.av-blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
|
||||
.av-violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
|
||||
.av-amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); color: var(--av-amber-fg); }
|
||||
.av-rose { background: linear-gradient(135deg, var(--av-rose-1), var(--av-rose-2)); }
|
||||
|
||||
main { display: flex; flex-direction: column; gap: 24px; }
|
||||
.greeting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
||||
.greeting { font-family: var(--font-display); font-size: 26px; font-weight: 700; letter-spacing: -0.01em; }
|
||||
.actions { display: flex; align-items: center; gap: 12px; }
|
||||
.search { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 9px 14px; width: 260px; color: var(--fg-tertiary); font-size: 13px; }
|
||||
.search .kbd { margin-left: auto; font-size: 11px; border: 1px solid var(--border); padding: 1px 6px; border-radius: 4px; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; border-radius: var(--r-sm); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-family: var(--font-body); }
|
||||
.btn.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
|
||||
|
||||
.kpi-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
|
||||
@media (max-width: 1100px) { .kpi-strip { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 600px) { .kpi-strip { grid-template-columns: 1fr; } }
|
||||
.kpi { padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 150px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-card); box-shadow: var(--shadow-card); }
|
||||
.kpi-head { display: flex; align-items: center; gap: 10px; }
|
||||
.kpi-glyph { width: 32px; height: 32px; border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent-strong); display: inline-flex; align-items: center; justify-content: center; }
|
||||
.kpi-label { font-size: 13px; color: var(--fg-muted); font-weight: 500; flex: 1; }
|
||||
.kpi-menu { color: var(--fg-tertiary); }
|
||||
.kpi-num-row { display: flex; align-items: baseline; gap: 10px; }
|
||||
.kpi-num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1.1; letter-spacing: -0.015em; }
|
||||
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--r-pill); padding: 2px 8px; font-size: 11px; font-weight: 600; }
|
||||
.pill.up { background: var(--pill-up-bg); color: var(--pill-up-fg); }
|
||||
.pill.down { background: var(--pill-down-bg); color: var(--pill-down-fg); }
|
||||
.pill.avail { background: var(--pill-avail-bg); color: var(--pill-avail-fg); font-weight: 500; }
|
||||
.pill.unav { background: var(--pill-unav-bg); color: var(--pill-unav-fg); font-weight: 500; }
|
||||
.pill.leave { background: var(--pill-leave-bg); color: var(--pill-leave-fg); font-weight: 500; }
|
||||
.kpi-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; }
|
||||
.kpi-cap { font-size: 12px; color: var(--fg-tertiary); line-height: 1.4; max-width: 60%; }
|
||||
.kpi-strip-pat { height: 32px; flex-shrink: 0; border-radius: var(--r-xs); }
|
||||
.stripe-amber { background: repeating-linear-gradient(135deg, var(--stripe-amber) 0 8px, var(--surface) 8px 16px); width: 96px; }
|
||||
.stripe-blue { background: repeating-linear-gradient(135deg, var(--stripe-blue) 0 8px, var(--surface) 8px 16px); width: 120px; }
|
||||
.stripe-mint { background: repeating-linear-gradient(135deg, var(--stripe-mint) 0 8px, var(--surface) 8px 16px); width: 96px; }
|
||||
.kpi-mini-stat { font-size: 11px; color: var(--fg-tertiary); margin-top: 4px; text-align: right; }
|
||||
.kpi-rooms-list { display: flex; flex-direction: column; gap: 6px; margin-top: auto; }
|
||||
.kpi-rooms-list .row { display: flex; justify-content: space-between; font-size: 12px; }
|
||||
.kpi-rooms-list .row .lbl { color: var(--fg-muted); display: flex; align-items: center; gap: 6px; }
|
||||
.kpi-rooms-list .row .lbl::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--fg-tertiary); }
|
||||
.kpi-rooms-list .row .val { color: var(--fg); font-weight: 600; }
|
||||
|
||||
.mid-row { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
|
||||
@media (max-width: 1100px) { .mid-row { grid-template-columns: 1fr; } }
|
||||
.card-pad { padding: 20px; }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.card-title { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-size: 15px; font-weight: 600; }
|
||||
.card-title .ico-tile { width: 26px; height: 26px; border-radius: var(--r-sm); background: var(--surface-muted); color: var(--fg-muted); display: inline-flex; align-items: center; justify-content: center; }
|
||||
.dropdown { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12px; color: var(--fg-muted); background: var(--surface); }
|
||||
|
||||
.legend { display: flex; gap: 16px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.legend .lg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-muted); }
|
||||
.legend .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
|
||||
|
||||
.cal-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.cal-month { font-family: var(--font-display); font-size: 16px; font-weight: 600; }
|
||||
.cal-nav { color: var(--fg-muted); display: inline-flex; gap: 6px; }
|
||||
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; font-size: 12px; margin-top: 10px; position: relative; }
|
||||
.cal-grid .dow { color: var(--fg-tertiary); font-weight: 500; padding: 6px 0; font-size: 11px; }
|
||||
.cal-grid .day { height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-pill); }
|
||||
.cal-grid .day.muted { color: var(--fg-tertiary); }
|
||||
.cal-grid .day.active { background: var(--accent); color: var(--surface); font-weight: 700; position: relative; }
|
||||
.cal-grid .day.active::after { content: ""; position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: var(--r-pill); background: var(--accent); }
|
||||
.activity-popover { background: var(--surface-inverse); color: var(--fg-on-inverse); border-radius: var(--r-md); padding: 14px; box-shadow: var(--shadow-popover); margin-top: 14px; }
|
||||
.activity-popover h6 { margin: 0 0 10px; font-size: 12px; font-weight: 600; }
|
||||
.activity-popover .ev { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
||||
.activity-popover .ev:last-of-type { border-bottom: none; }
|
||||
.activity-popover .ev .av-pop { width: 22px; height: 22px; border-radius: var(--r-pill); flex-shrink: 0; }
|
||||
.activity-popover .ev .av-pop.blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
|
||||
.activity-popover .ev .av-pop.pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
|
||||
.activity-popover .ev .av-pop.violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
|
||||
.activity-popover .ev .av-pop.mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
|
||||
.activity-popover .ev .av-pop.amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); }
|
||||
.activity-popover .ev .name { flex: 1; font-size: 11px; }
|
||||
.activity-popover .ev .time { font-size: 10px; color: rgba(248,250,252,0.6); }
|
||||
.activity-popover .add { margin-top: 8px; font-size: 11px; color: rgba(248,250,252,0.7); }
|
||||
|
||||
.bottom-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
|
||||
@media (max-width: 1100px) { .bottom-row { grid-template-columns: 1fr; } }
|
||||
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 0; }
|
||||
.donut { position: relative; width: 180px; height: 180px; }
|
||||
.donut .center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
|
||||
.donut .center .lbl { font-size: 12px; color: var(--fg-tertiary); }
|
||||
.donut .center .num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1; margin-top: 2px; }
|
||||
.donut-legend { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; font-size: 12px; color: var(--fg-muted); }
|
||||
.donut-legend .lg { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.donut-legend .lg .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
|
||||
.donut-legend .lg .v { color: var(--fg); font-weight: 600; margin-right: 2px; }
|
||||
|
||||
.sched-stats { display: grid; grid-template-columns: 1fr 1fr 1fr; margin-bottom: 12px; }
|
||||
.sched-stats .col { padding: 4px 12px; border-left: 1px solid var(--border); }
|
||||
.sched-stats .col:first-child { border-left: none; padding-left: 0; }
|
||||
.sched-stats .col .v { font-family: var(--font-display); font-size: 18px; font-weight: 700; line-height: 1.1; }
|
||||
.sched-stats .col .v small { font-size: 11px; color: var(--fg-tertiary); font-weight: 400; margin-left: 2px; }
|
||||
.sched-stats .col .lbl { font-size: 11px; color: var(--fg-tertiary); margin-top: 2px; }
|
||||
|
||||
.list-head { display: flex; align-items: center; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 11px; color: var(--fg-tertiary); font-weight: 500; }
|
||||
.list-row { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.list-row:last-child { border-bottom: none; }
|
||||
.person { flex: 1; display: flex; flex-direction: column; }
|
||||
.person .n { font-size: 13px; font-weight: 600; }
|
||||
.person .r { font-size: 11px; color: var(--fg-tertiary); }
|
||||
.pill.list { padding: 3px 10px; }
|
||||
.appt-time { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.appt-time .d { font-size: 11px; color: var(--fg-tertiary); }
|
||||
.appt-time .t { font-size: 13px; font-weight: 600; }
|
||||
|
||||
.ic { width: 18px; height: 18px; stroke-width: 1.6; }
|
||||
.ic-sm { width: 14px; height: 14px; stroke-width: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-od-id="clinic-console-template">
|
||||
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
||||
<defs>
|
||||
<symbol id="icon-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></symbol>
|
||||
<symbol id="icon-message" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 1 1 21 11.5z"/></symbol>
|
||||
<symbol id="icon-schedule" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></symbol>
|
||||
<symbol id="icon-card" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></symbol>
|
||||
<symbol id="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a8 8 0 0 1 16 0v1"/></symbol>
|
||||
<symbol id="icon-pill" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10 2v4M14 2v4M5 10h14M6 6h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM12 13v6M9 16h6"/></symbol>
|
||||
<symbol id="icon-bed" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 17h18M5 17v-5a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5"/></symbol>
|
||||
<symbol id="icon-check-square" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M8 14l3 3 5-5"/></symbol>
|
||||
<symbol id="icon-people" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="9" cy="8" r="3"/><path d="M3 21v-1a6 6 0 0 1 12 0v1"/><circle cx="17" cy="6" r="2.5"/><path d="M14 14a4 4 0 0 1 7 4"/></symbol>
|
||||
<symbol id="icon-leaf" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12c4-8 14-8 14 0 0 6-7 8-7 8s-7-2-7-8z"/></symbol>
|
||||
<symbol id="icon-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/></symbol>
|
||||
<symbol id="icon-stethoscope" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 1 1 8 0v4"/></symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></symbol>
|
||||
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></symbol>
|
||||
<symbol id="icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="icon-chev-left" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 18l-6-6 6-6"/></symbol>
|
||||
<symbol id="icon-chev-right" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 18l6-6-6-6"/></symbol>
|
||||
<symbol id="icon-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 9l6 6 6-6"/></symbol>
|
||||
<symbol id="icon-collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 6l-6 6 6 6M21 6l-6 6 6 6"/></symbol>
|
||||
<symbol id="icon-logout" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<aside class="sidebar" data-od-id="sidebar">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">
|
||||
<span class="brand-leaf"><svg class="ic-sm"><use href="#icon-leaf"/></svg></span>
|
||||
{{data.brand_name}}
|
||||
</div>
|
||||
<span class="brand-toggle"><svg class="ic"><use href="#icon-collapse"/></svg></span>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="nav-label">{{data.nav_main_label}}</div>
|
||||
<a class="nav-item {{data.nav_main.0.active_class}}"><svg class="ic"><use href="#icon-dashboard"/></svg>{{data.nav_main.0.label}}<span class="count">{{data.nav_main.0.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_main.1.active_class}}"><svg class="ic"><use href="#icon-message"/></svg>{{data.nav_main.1.label}}<span class="count">{{data.nav_main.1.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_main.2.active_class}}"><svg class="ic"><use href="#icon-schedule"/></svg>{{data.nav_main.2.label}}<span class="count">{{data.nav_main.2.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_main.3.active_class}}"><svg class="ic"><use href="#icon-bell"/></svg>{{data.nav_main.3.label}}<span class="count">{{data.nav_main.3.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_main.4.active_class}}"><svg class="ic"><use href="#icon-card"/></svg>{{data.nav_main.4.label}}<span class="count">{{data.nav_main.4.count}}</span></a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="nav-label">{{data.nav_management_label}}</div>
|
||||
<a class="nav-item {{data.nav_management.0.active_class}}"><svg class="ic"><use href="#icon-user"/></svg>{{data.nav_management.0.label}}<span class="count">{{data.nav_management.0.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_management.1.active_class}}"><svg class="ic"><use href="#icon-pill"/></svg>{{data.nav_management.1.label}}<span class="count">{{data.nav_management.1.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_management.2.active_class}}"><svg class="ic"><use href="#icon-bed"/></svg>{{data.nav_management.2.label}}<span class="count">{{data.nav_management.2.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_management.3.active_class}}"><svg class="ic"><use href="#icon-check-square"/></svg>{{data.nav_management.3.label}}<span class="count">{{data.nav_management.3.count}}</span></a>
|
||||
<a class="nav-item {{data.nav_management.4.active_class}}"><svg class="ic"><use href="#icon-people"/></svg>{{data.nav_management.4.label}}<span class="count">{{data.nav_management.4.count}}</span></a>
|
||||
</div>
|
||||
|
||||
<div class="pro-card">
|
||||
<span class="pro-tag">{{data.pro_card.tag}}</span>
|
||||
<h4>{{data.pro_card.title}}</h4>
|
||||
<p>{{data.pro_card.body}}</p>
|
||||
<div class="pro-actions">
|
||||
<button class="primary">{{data.pro_card.primary_label}}</button>
|
||||
<button>{{data.pro_card.secondary_label}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-row">
|
||||
<span class="av av-32 {{data.user.av_class}}">{{data.user.initial}}</span>
|
||||
<div class="meta">
|
||||
<span class="name">{{data.user.name}}</span>
|
||||
<span class="role">{{data.user.role}}</span>
|
||||
</div>
|
||||
<span class="out"><svg class="ic"><use href="#icon-logout"/></svg></span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
|
||||
<header class="greeting-row" data-od-id="greeting">
|
||||
<div class="greeting">{{data.greeting}}</div>
|
||||
<div class="actions">
|
||||
<div class="search">
|
||||
<svg class="ic-sm"><use href="#icon-search"/></svg>
|
||||
{{data.search_placeholder}}
|
||||
<span class="kbd">{{data.search_shortcut}}</span>
|
||||
</div>
|
||||
<button class="btn"><svg class="ic-sm"><use href="#icon-download"/></svg>{{data.secondary_action_label}}</button>
|
||||
<button class="btn primary"><svg class="ic-sm"><use href="#icon-plus"/></svg>{{data.primary_action_label}}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="kpi-strip" data-od-id="kpi-strip">
|
||||
<div class="kpi">
|
||||
<div class="kpi-head">
|
||||
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-user"/></svg></span>
|
||||
<span class="kpi-label">{{data.kpi_a.label}}</span>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_a.value}}</div><span class="pill {{data.kpi_a.trend_class}}">{{data.kpi_a.trend_label}}</span></div>
|
||||
<div class="kpi-foot">
|
||||
<div class="kpi-cap">{{data.kpi_a.caption}}</div>
|
||||
<div class="kpi-strip-pat {{data.kpi_a.strip_class}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-head">
|
||||
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
|
||||
<span class="kpi-label">{{data.kpi_b.label}}</span>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_b.value}}</div><span class="pill {{data.kpi_b.trend_class}}">{{data.kpi_b.trend_label}}</span></div>
|
||||
<div class="kpi-foot">
|
||||
<div class="kpi-cap">{{data.kpi_b.caption}}</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;">
|
||||
<div class="kpi-strip-pat {{data.kpi_b.strip_class}}"></div>
|
||||
<div class="kpi-mini-stat">{{data.kpi_b.mini_stat}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-head">
|
||||
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-bed"/></svg></span>
|
||||
<span class="kpi-label">{{data.kpi_c.label}}</span>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_c.value}}</div><span class="pill {{data.kpi_c.trend_class}}">{{data.kpi_c.trend_label}}</span></div>
|
||||
<div class="kpi-rooms-list">
|
||||
<div class="row"><span class="lbl">{{data.kpi_c.rows.0.label}}</span><span class="val">{{data.kpi_c.rows.0.value}}</span></div>
|
||||
<div class="row"><span class="lbl">{{data.kpi_c.rows.1.label}}</span><span class="val">{{data.kpi_c.rows.1.value}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-head">
|
||||
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-people"/></svg></span>
|
||||
<span class="kpi-label">{{data.kpi_d.label}}</span>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_d.value}}</div><span class="pill {{data.kpi_d.trend_class}}">{{data.kpi_d.trend_label}}</span></div>
|
||||
<div class="kpi-foot">
|
||||
<div class="kpi-cap">{{data.kpi_d.caption}}</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;">
|
||||
<div class="kpi-strip-pat {{data.kpi_d.strip_class}}"></div>
|
||||
<div class="kpi-mini-stat">{{data.kpi_d.mini_stat}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mid-row" data-od-id="patient-overview-and-calendar">
|
||||
<div class="card card-pad">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-clock"/></svg></span>
|
||||
{{data.chart.title}}
|
||||
</div>
|
||||
<div class="dropdown">{{data.chart.dropdown_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span class="lg"><span class="dot" style="background: var(--accent);"></span>{{data.chart.legend_a}}</span>
|
||||
<span class="lg"><span class="dot" style="background: var(--legend-blue);"></span>{{data.chart.legend_b}}</span>
|
||||
<span class="lg"><span class="dot" style="background: var(--legend-amber);"></span>{{data.chart.legend_c}}</span>
|
||||
</div>
|
||||
|
||||
<svg viewBox="0 0 600 240" preserveAspectRatio="none" style="width:100%; height:240px; display:block;">
|
||||
<defs>
|
||||
<pattern id="stripeBlue" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
|
||||
<rect width="12" height="12" style="fill: var(--surface);" />
|
||||
<rect width="6" height="12" style="fill: var(--stripe-blue);" />
|
||||
</pattern>
|
||||
<pattern id="stripeMint" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
|
||||
<rect width="12" height="12" style="fill: var(--surface);" />
|
||||
<rect width="6" height="12" style="fill: var(--stripe-mint);" opacity="0.7" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<g style="stroke: var(--border);" stroke-width="1">
|
||||
<line x1="0" y1="48" x2="600" y2="48" />
|
||||
<line x1="0" y1="96" x2="600" y2="96" />
|
||||
<line x1="0" y1="144" x2="600" y2="144" />
|
||||
<line x1="0" y1="192" x2="600" y2="192" />
|
||||
</g>
|
||||
<g>
|
||||
<rect x="{{data.chart.bars.0.x}}" y="{{data.chart.bars.0.y}}" width="22" height="{{data.chart.bars.0.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.1.x}}" y="{{data.chart.bars.1.y}}" width="22" height="{{data.chart.bars.1.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
<rect x="{{data.chart.bars.2.x}}" y="{{data.chart.bars.2.y}}" width="22" height="{{data.chart.bars.2.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.3.x}}" y="{{data.chart.bars.3.y}}" width="22" height="{{data.chart.bars.3.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
<rect x="{{data.chart.bars.4.x}}" y="{{data.chart.bars.4.y}}" width="22" height="{{data.chart.bars.4.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.5.x}}" y="{{data.chart.bars.5.y}}" width="22" height="{{data.chart.bars.5.h}}" rx="6" fill="url(#stripeBlue)" style="stroke: var(--accent);" stroke-width="2" />
|
||||
<rect x="{{data.chart.bars.6.x}}" y="{{data.chart.bars.6.y}}" width="22" height="{{data.chart.bars.6.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.7.x}}" y="{{data.chart.bars.7.y}}" width="22" height="{{data.chart.bars.7.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
<rect x="{{data.chart.bars.8.x}}" y="{{data.chart.bars.8.y}}" width="22" height="{{data.chart.bars.8.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.9.x}}" y="{{data.chart.bars.9.y}}" width="22" height="{{data.chart.bars.9.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
<rect x="{{data.chart.bars.10.x}}" y="{{data.chart.bars.10.y}}" width="22" height="{{data.chart.bars.10.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.11.x}}" y="{{data.chart.bars.11.y}}" width="22" height="{{data.chart.bars.11.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
<rect x="{{data.chart.bars.12.x}}" y="{{data.chart.bars.12.y}}" width="22" height="{{data.chart.bars.12.h}}" rx="6" fill="url(#stripeMint)" />
|
||||
<rect x="{{data.chart.bars.13.x}}" y="{{data.chart.bars.13.y}}" width="22" height="{{data.chart.bars.13.h}}" rx="6" fill="url(#stripeBlue)" />
|
||||
</g>
|
||||
<g style="fill: var(--fg-tertiary);" font-size="11" text-anchor="middle">
|
||||
<text x="46" y="232">{{data.chart.x_labels.0}}</text>
|
||||
<text x="132" y="232">{{data.chart.x_labels.1}}</text>
|
||||
<text x="218" y="232">{{data.chart.x_labels.2}}</text>
|
||||
<text x="304" y="232">{{data.chart.x_labels.3}}</text>
|
||||
<text x="390" y="232">{{data.chart.x_labels.4}}</text>
|
||||
<text x="476" y="232">{{data.chart.x_labels.5}}</text>
|
||||
<text x="562" y="232">{{data.chart.x_labels.6}}</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" data-od-id="calendar">
|
||||
<div class="cal-head">
|
||||
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-left"/></svg></span>
|
||||
<div class="cal-month">{{data.calendar.month_label}}</div>
|
||||
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-right"/></svg></span>
|
||||
</div>
|
||||
<div class="cal-grid">
|
||||
<div class="dow">{{data.calendar.dow.0}}</div><div class="dow">{{data.calendar.dow.1}}</div><div class="dow">{{data.calendar.dow.2}}</div><div class="dow">{{data.calendar.dow.3}}</div><div class="dow">{{data.calendar.dow.4}}</div><div class="dow">{{data.calendar.dow.5}}</div><div class="dow">{{data.calendar.dow.6}}</div>
|
||||
<div class="day {{data.calendar.days.0.modifier}}">{{data.calendar.days.0.label}}</div><div class="day {{data.calendar.days.1.modifier}}">{{data.calendar.days.1.label}}</div><div class="day {{data.calendar.days.2.modifier}}">{{data.calendar.days.2.label}}</div><div class="day {{data.calendar.days.3.modifier}}">{{data.calendar.days.3.label}}</div><div class="day {{data.calendar.days.4.modifier}}">{{data.calendar.days.4.label}}</div><div class="day {{data.calendar.days.5.modifier}}">{{data.calendar.days.5.label}}</div><div class="day {{data.calendar.days.6.modifier}}">{{data.calendar.days.6.label}}</div>
|
||||
<div class="day {{data.calendar.days.7.modifier}}">{{data.calendar.days.7.label}}</div><div class="day {{data.calendar.days.8.modifier}}">{{data.calendar.days.8.label}}</div><div class="day {{data.calendar.days.9.modifier}}">{{data.calendar.days.9.label}}</div><div class="day {{data.calendar.days.10.modifier}}">{{data.calendar.days.10.label}}</div><div class="day {{data.calendar.days.11.modifier}}">{{data.calendar.days.11.label}}</div><div class="day {{data.calendar.days.12.modifier}}">{{data.calendar.days.12.label}}</div><div class="day {{data.calendar.days.13.modifier}}">{{data.calendar.days.13.label}}</div>
|
||||
<div class="day {{data.calendar.days.14.modifier}}">{{data.calendar.days.14.label}}</div><div class="day {{data.calendar.days.15.modifier}}">{{data.calendar.days.15.label}}</div><div class="day {{data.calendar.days.16.modifier}}">{{data.calendar.days.16.label}}</div><div class="day {{data.calendar.days.17.modifier}}">{{data.calendar.days.17.label}}</div><div class="day {{data.calendar.days.18.modifier}}">{{data.calendar.days.18.label}}</div><div class="day {{data.calendar.days.19.modifier}}">{{data.calendar.days.19.label}}</div><div class="day {{data.calendar.days.20.modifier}}">{{data.calendar.days.20.label}}</div>
|
||||
<div class="day {{data.calendar.days.21.modifier}}">{{data.calendar.days.21.label}}</div><div class="day {{data.calendar.days.22.modifier}}">{{data.calendar.days.22.label}}</div><div class="day {{data.calendar.days.23.modifier}}">{{data.calendar.days.23.label}}</div><div class="day {{data.calendar.days.24.modifier}}">{{data.calendar.days.24.label}}</div><div class="day {{data.calendar.days.25.modifier}}">{{data.calendar.days.25.label}}</div><div class="day {{data.calendar.days.26.modifier}}">{{data.calendar.days.26.label}}</div><div class="day {{data.calendar.days.27.modifier}}">{{data.calendar.days.27.label}}</div>
|
||||
<div class="day {{data.calendar.days.28.modifier}}">{{data.calendar.days.28.label}}</div><div class="day {{data.calendar.days.29.modifier}}">{{data.calendar.days.29.label}}</div><div class="day {{data.calendar.days.30.modifier}}">{{data.calendar.days.30.label}}</div><div class="day {{data.calendar.days.31.modifier}}">{{data.calendar.days.31.label}}</div><div class="day {{data.calendar.days.32.modifier}}">{{data.calendar.days.32.label}}</div><div class="day {{data.calendar.days.33.modifier}}">{{data.calendar.days.33.label}}</div><div class="day {{data.calendar.days.34.modifier}}">{{data.calendar.days.34.label}}</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-popover" data-od-id="activity-popover">
|
||||
<h6>{{data.activity.title}}</h6>
|
||||
<div class="ev"><span class="av-pop {{data.activity.events.0.av_class}}"></span><span class="name">{{data.activity.events.0.name}}</span><span class="time">{{data.activity.events.0.time}}</span></div>
|
||||
<div class="ev"><span class="av-pop {{data.activity.events.1.av_class}}"></span><span class="name">{{data.activity.events.1.name}}</span><span class="time">{{data.activity.events.1.time}}</span></div>
|
||||
<div class="ev"><span class="av-pop {{data.activity.events.2.av_class}}"></span><span class="name">{{data.activity.events.2.name}}</span><span class="time">{{data.activity.events.2.time}}</span></div>
|
||||
<div class="add">{{data.activity.add_label}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bottom-row">
|
||||
|
||||
<div class="card card-pad" data-od-id="top-clinics">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-stethoscope"/></svg></span>
|
||||
{{data.donut.title}}
|
||||
</div>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="donut-wrap">
|
||||
<div class="donut">
|
||||
<svg viewBox="0 0 100 100" style="width:100%; height:100%; transform: rotate(-90deg);">
|
||||
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-blue);" />
|
||||
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-pink);" stroke-dasharray="{{data.donut.segment_b.dasharray}}" stroke-dashoffset="{{data.donut.segment_b.dashoffset}}" />
|
||||
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-mint);" stroke-dasharray="{{data.donut.segment_c.dasharray}}" stroke-dashoffset="{{data.donut.segment_c.dashoffset}}" />
|
||||
</svg>
|
||||
<div class="center">
|
||||
<span class="lbl">{{data.donut.center_label}}</span>
|
||||
<span class="num">{{data.donut.center_num}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="donut-legend">
|
||||
<div class="lg"><span class="dot" style="background: var(--legend-blue);"></span><span class="v">{{data.donut.legend.0.value}}</span> {{data.donut.legend.0.label}}</div>
|
||||
<div class="lg"><span class="dot" style="background: var(--legend-pink);"></span><span class="v">{{data.donut.legend.1.value}}</span> {{data.donut.legend.1.label}}</div>
|
||||
<div class="lg"><span class="dot" style="background: var(--legend-mint);"></span><span class="v">{{data.donut.legend.2.value}}</span> {{data.donut.legend.2.label}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" data-od-id="doctor-schedule">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
|
||||
{{data.schedule.title}}
|
||||
</div>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
|
||||
<div class="sched-stats">
|
||||
<div class="col"><div class="v">{{data.schedule.stats.0.value}} <small>{{data.schedule.stats.0.small}}</small></div><div class="lbl">{{data.schedule.stats.0.label}}</div></div>
|
||||
<div class="col"><div class="v">{{data.schedule.stats.1.value}} <small>{{data.schedule.stats.1.small}}</small></div><div class="lbl">{{data.schedule.stats.1.label}}</div></div>
|
||||
<div class="col"><div class="v">{{data.schedule.stats.2.value}} <small>{{data.schedule.stats.2.small}}</small></div><div class="lbl">{{data.schedule.stats.2.label}}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="list-head">{{data.schedule.list_header_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
|
||||
<div class="list-row">
|
||||
<span class="av av-32 {{data.schedule.doctors.0.av_class}}">{{data.schedule.doctors.0.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.schedule.doctors.0.name}}</span><span class="r">{{data.schedule.doctors.0.role}}</span></div>
|
||||
<span class="pill list {{data.schedule.doctors.0.status_class}}">{{data.schedule.doctors.0.status_label}}</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-32 {{data.schedule.doctors.1.av_class}}">{{data.schedule.doctors.1.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.schedule.doctors.1.name}}</span><span class="r">{{data.schedule.doctors.1.role}}</span></div>
|
||||
<span class="pill list {{data.schedule.doctors.1.status_class}}">{{data.schedule.doctors.1.status_label}}</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-32 {{data.schedule.doctors.2.av_class}}">{{data.schedule.doctors.2.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.schedule.doctors.2.name}}</span><span class="r">{{data.schedule.doctors.2.role}}</span></div>
|
||||
<span class="pill list {{data.schedule.doctors.2.status_class}}">{{data.schedule.doctors.2.status_label}}</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-32 {{data.schedule.doctors.3.av_class}}">{{data.schedule.doctors.3.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.schedule.doctors.3.name}}</span><span class="r">{{data.schedule.doctors.3.role}}</span></div>
|
||||
<span class="pill list {{data.schedule.doctors.3.status_class}}">{{data.schedule.doctors.3.status_label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" data-od-id="appointments">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-check-square"/></svg></span>
|
||||
{{data.appointments.title}}
|
||||
</div>
|
||||
<span class="kpi-menu">⋯</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-40 {{data.appointments.list.0.av_class}}">{{data.appointments.list.0.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.appointments.list.0.name}}</span><span class="r">{{data.appointments.list.0.role}}</span></div>
|
||||
<div class="appt-time"><span class="d">{{data.appointments.list.0.date}}</span><span class="t">{{data.appointments.list.0.time}}</span></div>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-40 {{data.appointments.list.1.av_class}}">{{data.appointments.list.1.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.appointments.list.1.name}}</span><span class="r">{{data.appointments.list.1.role}}</span></div>
|
||||
<div class="appt-time"><span class="d">{{data.appointments.list.1.date}}</span><span class="t">{{data.appointments.list.1.time}}</span></div>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-40 {{data.appointments.list.2.av_class}}">{{data.appointments.list.2.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.appointments.list.2.name}}</span><span class="r">{{data.appointments.list.2.role}}</span></div>
|
||||
<div class="appt-time"><span class="d">{{data.appointments.list.2.date}}</span><span class="t">{{data.appointments.list.2.time}}</span></div>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-40 {{data.appointments.list.3.av_class}}">{{data.appointments.list.3.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.appointments.list.3.name}}</span><span class="r">{{data.appointments.list.3.role}}</span></div>
|
||||
<div class="appt-time"><span class="d">{{data.appointments.list.3.date}}</span><span class="t">{{data.appointments.list.3.time}}</span></div>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="av av-40 {{data.appointments.list.4.av_class}}">{{data.appointments.list.4.initial}}</span>
|
||||
<div class="person"><span class="n">{{data.appointments.list.4.name}}</span><span class="r">{{data.appointments.list.4.role}}</span></div>
|
||||
<div class="appt-time"><span class="d">{{data.appointments.list.4.date}}</span><span class="t">{{data.appointments.list.4.time}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue