fenix-claude-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +11 -0
- package/README.md +55 -0
- package/artifacts/practice-dashboard.html +469 -0
- package/artifacts/project-overview.html +481 -0
- package/artifacts/user-dashboard.html +368 -0
- package/commands/fenix-install.md +15 -0
- package/commands/fenix-setup.md +14 -0
- package/package.json +16 -0
- package/skills/fenix-application/CUSTOMIZE.md +1 -0
- package/skills/fenix-application/SKILL.md +95 -0
- package/skills/fenix-application/VERSION +1 -0
- package/skills/fenix-application/assets/claims_editor.html +87 -0
- package/skills/fenix-application/assets/figures_editor.html +87 -0
- package/skills/fenix-application/assets/spec_editor.html +87 -0
- package/skills/fenix-application/references/claims.md +30 -0
- package/skills/fenix-application/references/disclosure.md +33 -0
- package/skills/fenix-application/references/editors.md +52 -0
- package/skills/fenix-application/references/figure-description.md +67 -0
- package/skills/fenix-application/references/figures-guidance.md +122 -0
- package/skills/fenix-application/references/figures.md +28 -0
- package/skills/fenix-application/references/patent-review-advantages.md +35 -0
- package/skills/fenix-application/references/patent-review-checklist.md +36 -0
- package/skills/fenix-application/references/patent-review-claim-formats.md +71 -0
- package/skills/fenix-application/references/patent-review-profanity.md +57 -0
- package/skills/fenix-application/references/patent-review.md +82 -0
- package/skills/fenix-application/references/patent-safe.md +44 -0
- package/skills/fenix-application/references/patent.md +18 -0
- package/skills/fenix-application/references/spec-guidance.md +195 -0
- package/skills/fenix-application/references/spec.md +48 -0
- package/skills/fenix-application/scripts/calculate_figure_layout.py +32 -0
- package/skills/fenix-application/scripts/parse_claims.py +39 -0
- package/skills/fenix-office-action/CUSTOMIZE.md +1 -0
- package/skills/fenix-office-action/SKILL.md +40 -0
- package/skills/fenix-office-action/VERSION +1 -0
- package/skills/fenix-office-action/references/oa-response.md +10 -0
- package/skills/fenix-project/CUSTOMIZE.md +1 -0
- package/skills/fenix-project/SKILL.md +41 -0
- package/skills/fenix-project/VERSION +1 -0
- package/skills/fenix-project/references/create-project.md +10 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fenix",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fenix patent automation for Claude — MCP connector plus patent drafting, office-action, and project skills. Provide one API key; the server infers your firm (client + database).",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Fenix AI",
|
|
7
|
+
"url": "https://fenix.ai"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://claude.fenix.ai",
|
|
10
|
+
"keywords": ["patent", "legal", "fenix", "mcp", "drafting"],
|
|
11
|
+
"skills": "./skills/",
|
|
12
|
+
"mcpServers": "./.mcp.json"
|
|
13
|
+
}
|
package/.mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Fenix plugin for Claude
|
|
2
|
+
|
|
3
|
+
Patent automation for Claude Code / Cowork. Bundles:
|
|
4
|
+
|
|
5
|
+
- **The Fenix MCP connector** — `https://claude.fenix.ai/api/plugin` (read + draft tools: `get_project`, `save_project`, `print_patent`, `install_fenix_skills`, and more).
|
|
6
|
+
- **The patent skills** — `fenix-application`, `fenix-office-action`, `fenix-project` — shipped as a static snapshot so they work immediately, with a `/fenix-install` command to refresh the firm-customized, latest versions from the server.
|
|
7
|
+
|
|
8
|
+
## One key, nothing else
|
|
9
|
+
|
|
10
|
+
You provide a **single API key**. The server infers your firm — client *and* database — from that key, so you never configure a database or client name.
|
|
11
|
+
|
|
12
|
+
## Install (no git required)
|
|
13
|
+
|
|
14
|
+
The marketplace is served over plain HTTPS and the plugin is distributed via npm,
|
|
15
|
+
so end users never touch git:
|
|
16
|
+
|
|
17
|
+
1. Add the marketplace and install the plugin:
|
|
18
|
+
```
|
|
19
|
+
claude plugin marketplace add https://claude.fenix.ai/marketplace.json
|
|
20
|
+
claude plugin install fenix@fenix
|
|
21
|
+
```
|
|
22
|
+
2. Set your key (see `/fenix-setup` for guided steps):
|
|
23
|
+
- macOS/Linux: `export FENIX_API_KEY="<your key>"` in `~/.zshrc` / `~/.bashrc`
|
|
24
|
+
- Windows: `setx FENIX_API_KEY "<your key>"`
|
|
25
|
+
Restart Claude Code so the connector picks up the key.
|
|
26
|
+
3. (Optional) `/fenix-install` — refresh the skills to your firm's latest customized versions.
|
|
27
|
+
|
|
28
|
+
> Local/dev install (from a clone of this repo, no npm needed):
|
|
29
|
+
> `claude plugin marketplace add <path-to-Project-Management>` then `claude plugin install fenix@fenix`
|
|
30
|
+
> — this uses the repo-root `.claude-plugin/marketplace.json` (relative source).
|
|
31
|
+
|
|
32
|
+
## Publishing a new version (maintainers)
|
|
33
|
+
|
|
34
|
+
The plugin ships to users as the npm package **`fenix-claude-plugin`**, referenced by the
|
|
35
|
+
HTTPS marketplace at `https://claude.fenix.ai/marketplace.json` (served by
|
|
36
|
+
`back-end/pages/api/marketplace.ts`).
|
|
37
|
+
|
|
38
|
+
1. Regenerate the bundled snapshot: `cd back-end && npm run gen:snapshot`.
|
|
39
|
+
2. Bump the version in **three** places (keep them identical):
|
|
40
|
+
- `fenix-plugin/package.json` → `version`
|
|
41
|
+
- `fenix-plugin/.claude-plugin/plugin.json` → `version`
|
|
42
|
+
- `back-end/pages/api/marketplace.ts` → `PLUGIN_VERSION`
|
|
43
|
+
3. Publish: `cd fenix-plugin && npm publish` (unscoped public package — no scope/org needed).
|
|
44
|
+
4. Deploy the back-end so `/marketplace.json` and the `/api/plugin` connector are live.
|
|
45
|
+
|
|
46
|
+
Verify the npm contents before publishing with `npm pack --dry-run`.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
- `/fenix-setup` — guided API-key configuration.
|
|
51
|
+
- `/fenix-install` — pull the latest, firm-customized skills from the server.
|
|
52
|
+
|
|
53
|
+
## How the skills stay in sync
|
|
54
|
+
|
|
55
|
+
The bundled `skills/` snapshot is **generated from the same base files the server serves** (`back-end/skills/…`) — it is never hand-edited. Editing a base file and regenerating updates both the server's dynamic install and this static snapshot, so the two never drift. See `back-end/skills/README.md` for the source of truth and the regeneration script.
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Practice Dashboard</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.js" integrity="sha384-iU8HYtnGQ8Cy4zl7gbNMOhsDTTKX02BTXptVP/vqAWIaTfM7isw76iyZCsjL2eVi" crossorigin="anonymous"></script>
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/gridjs@5.0.2/dist/gridjs.umd.js" integrity="sha384-/XXDzxe4FsGiAe50i/u9pY/Vy/uX654MHB1xoc1BJNnH1WXHhqHga9g3q5tF4gj7" crossorigin="anonymous"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridjs@5.0.2/dist/theme/mermaid.min.css" integrity="sha384-jZvDSsmGB9oGGT/4l9bHXGoAv1OxvG/cFmSo0dZaSqmBgvQTKDBFAMftlXTmMbNW" crossorigin="anonymous">
|
|
10
|
+
<style>
|
|
11
|
+
:root{
|
|
12
|
+
color-scheme: dark;
|
|
13
|
+
--bg:#0c0e13; --card:#14171f; --card2:#191d27; --ink:#c3c8d4; --muted:#7e8598;
|
|
14
|
+
--line:#242936; --line2:#2f3543;
|
|
15
|
+
--accent:#818cf8; --accent2:#6366f1; --accentbg:#1c2030; --accentglow:#2a2f4a;
|
|
16
|
+
--red:#f87171; --redbg:#2a1417; --orange:#fb923c; --orangebg:#2a1c10;
|
|
17
|
+
--amber:#fbbf24; --amberbg:#2a230f; --green:#34d399; --greenbg:#0f2a20;
|
|
18
|
+
--gray:#8b92a6; --graybg:#1b1f29; --purple:#a78bfa; --purplebg:#211a33;
|
|
19
|
+
}
|
|
20
|
+
*{box-sizing:border-box}
|
|
21
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
22
|
+
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
23
|
+
.wrap{max-width:1280px;margin:0 auto;padding:22px 20px 80px}
|
|
24
|
+
.top{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:4px}
|
|
25
|
+
.brand{display:flex;align-items:center;gap:13px}
|
|
26
|
+
.brand .logo{display:inline-flex;align-items:center;justify-content:center;width:46px;height:46px;border-radius:11px;border:1px solid var(--line2);background:#0a0c11;line-height:0}
|
|
27
|
+
.brand .logo img{display:block;width:38px;height:38px;object-fit:contain}
|
|
28
|
+
h1{font-size:22px;margin:0;letter-spacing:-.02em;font-weight:800}
|
|
29
|
+
.sub{color:var(--muted);font-size:12.5px;margin-top:3px}
|
|
30
|
+
.acts{display:flex;gap:8px}
|
|
31
|
+
.btn{background:var(--accent2);color:#fff;border:0;border-radius:9px;padding:9px 15px;font-size:13px;font-weight:600;cursor:pointer}
|
|
32
|
+
.btn:hover{background:var(--accent)}
|
|
33
|
+
.btn.ghost{background:var(--card2);color:var(--ink);border:1px solid var(--line2)}
|
|
34
|
+
.prog{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 16px;min-height:24px}
|
|
35
|
+
.chip{display:flex;align-items:center;gap:7px;background:var(--card);border:1px solid var(--line);border-radius:20px;padding:4px 11px;font-size:11.5px;color:var(--muted)}
|
|
36
|
+
.chip.done{color:var(--green);border-color:transparent;background:var(--greenbg)}
|
|
37
|
+
.chip.err{color:var(--red);border-color:transparent;background:var(--redbg)}
|
|
38
|
+
.chip .sp{width:11px;height:11px;border:2px solid var(--line2);border-top-color:var(--accent);border-radius:50%;animation:s .7s linear infinite;display:inline-block}
|
|
39
|
+
.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:11px;margin-bottom:20px}
|
|
40
|
+
.kpi{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:15px 16px;position:relative;overflow:hidden}
|
|
41
|
+
.kpi .n{font-size:30px;font-weight:800;letter-spacing:-.03em;line-height:1}
|
|
42
|
+
.kpi .l{color:var(--muted);font-size:11.5px;margin-top:7px}
|
|
43
|
+
.kpi.red{background:linear-gradient(180deg,var(--redbg),var(--card));border-color:#3a1f24}
|
|
44
|
+
.kpi.red .n{color:var(--red)}
|
|
45
|
+
.kpi.orange .n{color:var(--orange)}
|
|
46
|
+
.kpi.accent .n{color:var(--accent)}
|
|
47
|
+
.kpi.purple{background:linear-gradient(180deg,var(--purplebg),var(--card));border-color:#2e2547}
|
|
48
|
+
.kpi.purple .n{color:var(--purple)}
|
|
49
|
+
.ld{color:var(--muted);opacity:.4}
|
|
50
|
+
.tabs{display:flex;gap:20px;border-bottom:1px solid var(--line);margin-bottom:18px;flex-wrap:wrap}
|
|
51
|
+
.tabs button{border:0;background:transparent;color:var(--muted);padding:0 0 11px;font-size:14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500}
|
|
52
|
+
.tabs button:hover{color:var(--ink)}
|
|
53
|
+
.tabs button.on{color:var(--ink);border-bottom-color:var(--accent);font-weight:700}
|
|
54
|
+
.bar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:14px}
|
|
55
|
+
select{background:var(--card2);border:1px solid var(--line2);border-radius:9px;padding:8px 11px;color:var(--ink);font-size:13px}
|
|
56
|
+
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
|
|
57
|
+
.panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px}
|
|
58
|
+
.panel h3{margin:0 0 14px;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700}
|
|
59
|
+
.ai{background:linear-gradient(180deg,var(--accentglow),var(--card));border:1px solid var(--line2);border-radius:14px;margin-bottom:18px;overflow:hidden;display:none}
|
|
60
|
+
.ai.show{display:block}
|
|
61
|
+
.ai .hd{padding:13px 17px;border-bottom:1px solid var(--line);font-weight:700;font-size:13px;display:flex;align-items:center;gap:8px}
|
|
62
|
+
.ai .bd{padding:15px 17px;font-size:13.5px;white-space:pre-wrap;line-height:1.6}
|
|
63
|
+
.dot{width:7px;height:7px;border-radius:50%;background:var(--accent);display:inline-block;box-shadow:0 0 8px var(--accent)}
|
|
64
|
+
.pill{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap}
|
|
65
|
+
.flag{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:6px;font-size:10px;font-weight:800;background:var(--purplebg);color:var(--purple)}
|
|
66
|
+
.due{font-weight:700;white-space:nowrap}
|
|
67
|
+
.days{display:inline-block;margin-left:7px;padding:1px 8px;border-radius:6px;font-size:10.5px;font-weight:700}
|
|
68
|
+
.lk{color:var(--accent);text-decoration:none;font-weight:700;font-size:12px;white-space:nowrap}
|
|
69
|
+
.lk:hover{text-decoration:underline}
|
|
70
|
+
.center{text-align:center;color:var(--muted);padding:60px 0}
|
|
71
|
+
.spin{width:26px;height:26px;border:3px solid var(--line2);border-top-color:var(--accent);border-radius:50%;animation:s .7s linear infinite;margin:0 auto 13px}
|
|
72
|
+
@keyframes s{to{transform:rotate(360deg)}}
|
|
73
|
+
.typ{color:var(--muted);font-size:12px}
|
|
74
|
+
.atable{width:100%;border-collapse:separate;border-spacing:0}
|
|
75
|
+
.atable td{padding:11px 8px;border-top:1px solid var(--line);vertical-align:top}
|
|
76
|
+
.atable tr:first-child td{border-top:0}
|
|
77
|
+
.title{font-weight:700}
|
|
78
|
+
.meta{color:var(--muted);font-size:12px;margin-top:2px}
|
|
79
|
+
.ucard{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--line);border-radius:13px;background:var(--card);margin-bottom:10px}
|
|
80
|
+
.av{width:42px;height:42px;border-radius:50%;background:var(--accentbg);color:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px;flex-shrink:0;border:1px solid var(--line2)}
|
|
81
|
+
.ucard .grow{flex:1}
|
|
82
|
+
.hbar{height:7px;border-radius:5px;background:var(--graybg);overflow:hidden;margin-top:7px}
|
|
83
|
+
.hbar > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:5px}
|
|
84
|
+
.num{font-weight:800;font-size:17px}
|
|
85
|
+
canvas{max-height:280px}
|
|
86
|
+
/* Grid.js dark theme — override defaults */
|
|
87
|
+
.gridjs-container{color:var(--ink) !important;width:100%;background:transparent !important}
|
|
88
|
+
.gridjs-wrapper{border:1px solid var(--line) !important;border-radius:12px;box-shadow:none !important;background:var(--card) !important}
|
|
89
|
+
table.gridjs-table{background:var(--card) !important}
|
|
90
|
+
th.gridjs-th{background:var(--card2) !important;color:var(--muted) !important;border-color:var(--line) !important;font-size:11px;text-transform:uppercase;letter-spacing:.04em;font-weight:700;padding:11px 13px}
|
|
91
|
+
th.gridjs-th-sort:hover,th.gridjs-th-sort:focus{background:var(--line) !important}
|
|
92
|
+
.gridjs-th .gridjs-th-content{color:var(--muted) !important}
|
|
93
|
+
td.gridjs-td{background:var(--card) !important;color:var(--ink) !important;border-color:var(--line) !important;padding:11px 13px;font-size:13px}
|
|
94
|
+
.gridjs-tr:hover td.gridjs-td{background:var(--accentbg) !important}
|
|
95
|
+
.gridjs-tr-selected td.gridjs-td{background:var(--accentbg) !important}
|
|
96
|
+
input.gridjs-search-input,.gridjs-input{background:var(--card2) !important;border:1px solid var(--line2) !important;color:var(--ink) !important;border-radius:9px;height:36px}
|
|
97
|
+
.gridjs-search-input::placeholder{color:var(--muted) !important}
|
|
98
|
+
.gridjs-pagination{color:var(--muted) !important}
|
|
99
|
+
.gridjs-pagination .gridjs-pages button{background:var(--card2) !important;color:var(--ink) !important;border:1px solid var(--line2) !important}
|
|
100
|
+
.gridjs-pagination .gridjs-pages button:hover{background:var(--line) !important}
|
|
101
|
+
.gridjs-pagination .gridjs-pages button.gridjs-currentPage{background:var(--accent2) !important;color:#fff !important;border-color:var(--accent2) !important;font-weight:700}
|
|
102
|
+
.gridjs-footer,.gridjs-head{background:transparent !important;border:0 !important;box-shadow:none !important}
|
|
103
|
+
.gridjs-head{margin-bottom:10px}
|
|
104
|
+
.gridjs-loading-bar{background:var(--card2) !important;opacity:.6}
|
|
105
|
+
.gridjs *{box-shadow:none !important}
|
|
106
|
+
@media(max-width:980px){ .kpis{grid-template-columns:repeat(3,1fr)} .grid2{grid-template-columns:1fr} }
|
|
107
|
+
@media(max-width:560px){ .kpis{grid-template-columns:repeat(2,1fr)} }
|
|
108
|
+
</style>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<div class="wrap">
|
|
112
|
+
<div class="top">
|
|
113
|
+
<div class="brand">
|
|
114
|
+
<span class="logo" aria-label="Fenix logo" role="img"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAmNUlEQVR4nO2deZwlVXn3v8+puvu93dMzzAAzgELCIkFFoigqCYokgsYF8XVfY9QYiUlcE7NojMbgHmNwS5REjTsEBEGJbCoi+zYMMwyzrz0zvd696jzvH6fq9nZv9+1hoPu29ft8mmbq1j11qurXz3Oe9YiqkiDBQsEs9AQS/GYjIWCCBUVCwAQLioSACRYUCQETLCgSAiZYUCQETLCgSAiYYEGREDDBgiIhYIIFRULABAuKhIAJFhQJARMsKBICJlhQJARMsKBICJhgQZEQMMGCIiFgggVFQsAEC4qEgPNFhxqapLbm4CDJg0uwkEgk4Dyh5eH2x6tj0Kx1lJAJ2iMhYDdQBVWC3Ru1cfuPtXUMQC0A4d4t2rj7WkUErF2omfYcEgJ2AVULItSvuwStDEUHY5IJAKY0QO2Gb2HLwyCSSMIukRBwLqgiYqAyQvXGb2OWr2l7mulfJcGuB2nccpmTgppIwW6QEHBOKIhQve4STY3uwhx2lDssMvU0L4X0raL8o89Bsw5iEinYBRICzgJVCwhaHaXy4y/R9PNIaeXUkyZJO2/FGgaGNlK/7UeJFOwSCQFngaiTfrWff0f7xrbBwCok3x+JvgkJGLuyvGVHYESo/OTLYINECnaBhICdoApisNVxKtd8CUTw+w9HMvkZp8ZUNANHUG0quuEW6ndck0jBLpAQsBMiy7dx2xXq7V5PI1Rk+RrET7Ws4umQ0nKaqqQlpHrNl6ODiRScDQkB2yGSfjSqVH58MaKKAN6yVROfT0ZERm/5GurWUA2F8IGbCNbelEjBOZAQsB1i6Xff9Sqb76SmHqCYgSOByau/qTDLV0PWqei8Cahc80X3QRtpmcAhIWA7GANhQPmaL5PxBBGDVcHrX9X+/Ihgkl+GyfWREqUcCMHd1xJsuVcRg9rwMbyB3kFCwGlwRBGCDbeorr2ecgjYkBCF4vIOX3K/TK4kpnQYnoAVj7ytUv3pVwCQRAq2RULAaYiJUr3hG+QIwXgYsdTVw5RWAC2+Tf6SWxems5j+w/FEQJRqoDRuvZxw/3aFZC3YDgkBJyNyPAfbH9D6LZdRsaChxQCSziERAdtLM0dLKQ04jaxKQw256gHqP/8OiCQ5g22QEHASNHI813/xPYpBmTB6PJ6A5PswpeUznNDTYZYd3vp/AQKr1K6/BK2MIMZLXDLT8JtBwG5euipiPHR8iOrN36ceKlj3PSNgSiuRbGHOa5iBI1o6WrDUQoM/uInG3ddGaVxdquHfEKL+ZhCwGwMgIkbtlss0t38zdWsQLBjBF8FfeRTiZyIf4SwSsLTC8dbGlongiVC99mtgQ+dfPFRzXgJY8gTUZg07um9ucWIMqKX+yx9G/54ggBGQ/li1dhiq5YxeTdk66aeRE7oaQrD+FzQ33taVY1qro9j926Ymvi5RLF0CRi9Oxw5o896fTTk2A5Hrpbn+FrUbfkk1ZApJQnWZLm6I2QkhA6sxmTxevFpUxYqhKCH1my+ddR4aZ1dvW6vNDbdFB5e25bx0CRgRQOtlmg/eTJzX1/5cd7x207fJ08SKcZkwAFYJVTErjpo8bMcxTHE5ZEt4MiErVS21UKndcil2eI/SwRiJrxnsWE+4f/s8b7g3sXQJGEvAZp3m5rux1fKU45NOdOuyyij1u35KPdQWEVQEwdK0YDpFQSYNAyC5onj9q5za1oiUqjSsITuyi8bdP43O7yzZwr2bsINbJw+7ZLH0CTh+gGDXBhiL14HTXmlUQFS77UrNDG+nZqeu0YxA6KWRwjJ3YFYpqkg6h1mxGl9kyjpSjCACtV9dFh+Yqc6jscPdG7FDu6JDS9sYWbIEjF9uODJIpjpCOLxnyvHoH610qdrNP3D+PjP1kRhAsnlMX4coyNSLuu8Ul8/gaayGw7U3Em67XxFBaD+XcM8WbHkoul5CwJ5ELDl0eDclH8J9TqVNfZ1uXRgObtXm+l9RC3WK9BN1Uot0Hsn1ift+Z0LE5Ja+5TOIKqqE4lHQGvXbfxx/YcYYtjJCsGcjOroPbODuYwlbwkuWgLEICnasRwyE2x6YcYpG6rdx1zXkG6MEOsn4wEk7I2ByRSSVnfuS0W9TOqytqBScRd247zpQi05JVnW/7d7NSnUUWx5Gq+Pd3m3PYskSMFZddnALAMHuje6DSSrWhcYs9VuvcGebmdLNiGAKy5B0putrS66/raqO1XBzw60E29epROvG6EMAwl3rKXgQlocJ921rv25dQliaBFRFRNDqOHZoF40Awm33o80qLitFnfEhQji0S4PN97jQ23TL1Ih7QPkBML471oVRIJmCU8d2KnFElUANBVuj0cE3Ge51S4V0s0K4d7M7ZQl3WliaBIzV2fg+DYd2UbeKHdmLHRuKFmkTTt9g092kKgdoqkxRvzGMgImynLt1CksqQ6AT85jymXGryODBm+MD0e9oybB7Iwr4BuzeLdFHS9cQWaIEdLBDe7GVcRoh2OrYhG/N2pa1G6z7BRlPpqjmGCKRyZEtdnW9mG6SLxIoCOrCcZPHtErDKsGW+7DVsShNy7aIGA7txqoiCMGOB6eMuxSxNAkYr6f2bSUvTQLjkSXADu+e+Fxc7Lfx0K2EbdTlI4GksoQdrGUVpWnBHthBuHO9wkQERGtldHAroTqLOhzcOjHXJYqle2eA3b0RTwRjPDwRNHbuxp+P7lO7bxtN2z7EpqrOEs71tf7dFWY5T1SxGHLawEYSLlbtWh5SHRskVGct26Ht0KwvaVfM0iRgvJ7acneLWb5AOLgNAI2UmlZGCceHsOokU8fhfP/Qzi8yboJ926Yctvu3E1bHsDEBR/YSjuxprVuXIpYeAVs1vRXCXQ8TWEWto1w4tBOYcCbr+AFoNObWvo+C8BEBPbBjyrFw33byEqAYAgRTGSaM3UdL1BJeegQkXv9t1+bgJpoWwBJYdW4NG7QMDju2n5Q2sTJbfGPCYj7UsGP7J80Ywn3bJmLIYsh5QrDl3kfl2osFvUvAOZqFh3s3kapXnDFglUDBDm5Dxw5ovKjX6hhpT0Dbr7FiK1grI61/d4U5zhOczaOV8chn6bk573xwqrBVaG68w/1/Gys9uuHu5rRI0bsE7PCS46PBtnVkfUHFpdY3VZDyfoId61rn2vKIewBtIiBT0KX6a43SqOFHLph2vkW1ilXFVkcgbIJxhevB3k3Y2CK3SqCK7ngQrVdoOdBnXLS3F4c9S8BwaJdqsz7zg9gAWX/LtOOGnLE0J6k0HT8w6zVU1UmqejkepKu52eoY/qR8wLZjA1ofR4OG+/fwbg22P+iWDKIQuWuag1ux+3fMDMnFrpuhXeoIOg8rfRGh9wgYrcfqN36bcNdDUyrNYoeuHd2njY130JgUXosDE8G6myeGahGrPeJIrR0faZVsdvOStVHFN8wqWa0C9SoEDVCl+eCvyFb207AuISIO22WbYwRb7okGnnTtKI5c+fEXsCN7NZ5vr6EHCeheQnPtDTTuumbKsfh3sOlOvJEdNCaH19RSs0pz7Y2EezcrgB0fmvWttVRl+YBru0t3L1lr4zA12689bDSiCPXbryJlpiaxYgTfCI27fuL+HaeYRX9Udv92rd96BVqvxlfuYnaLCz1FQFWFKIPFDu2idvuVUyIF8atrrvsVWc+t/2KIKk1ryNeGacYvtFGb85qhAuMTBOwGdmRwdqKKcwtptK+Ilodp3H+Ty0ec7BOyStMqjQd+iZZHWgmrrdqR9b8mvX8TWh0FEhX8qKMVwRg7oOHoAeyWe7GDWydKHeP47sN3tP++ESxQvfn77oDnzUkUCy6rpjoyt0M4klB2ePectoECGjbA86n/+nItjO926pdJBo8oDQt2aCfhno0T68C4f82vL6fogx0dnHNqixU9RcBWlkt5COpjlGyN+p0uuzjuamWrYwT7NtO0ikzzMKtaKiGYDb+icf+NKtninAZwaN1aUUf2xoN0Pjmu6RjaM7t3RONU/yJaG9PKT79KOztbVAlVyNoG9kAUx7bROndwizbv+xn1EOzeTbPfxCJGTxGwpWLGDqCNBqEq1Zt/gIZNp5oBrY4p40OOONPCa6KKiiFjoHrVvyPpLKGlYyKCq+kV/LCOHR6MJ9FpcoCgtXHs4CaasyQ4CIJnwBQHaPz6csyWOykHTJV+k84VAVsfi64TZXHfdz3p6jChhXD3w50f2iJHTxFwIolgEF+bVALQzfdgdz+sEq/31KI2pJMHRGzIeKA07/k/6nf8mHKgbV9863wxpIzQ3LVhjtlF0nl4r9qhXY7YHePLNsqI2c34ZZ+eenOdEPki43Vt/Y6r8aKrhkN7Js2gt9BTBIyhlRFSnmBNinxYnbCGAcmWxBSW4wGdWKgKBDVk5/o5X5riumOFsf+w0+IuNgy2P4BXG6dJeyd0jMAC44OY0d00LLOeq4CJchLF8wl2btDmfddRsZFVXBuLptZ7r7P3ZgzYyqhbQwGBKpXrv+miBaqYfAmz4uiZLo1pUKDZzbI9SiANN92NrVeZq+t9uP0Bsh5d5fBZhWCWOjsVwWCpqTelML556xUUwwoqvksZq46iQZNe3KOuJwmojcjzj6VqDf7OtdTjnSrF4B19EjKXdcHsUmfSSTQthIObsIObOhcJxRb45nvmpQvnmoNvQEorMSuPablsKjd8g4bVlpVuK2PzchMtJvQmAStjE5pQnaCr/PAiqI0BSvr40wlCe0iKeZwlasiENYLNkRqeTprIALGVUYLNd9E4VBnWYkgbwT/2SZhlhwsiVK/9Dy3se4iaekhoUUCCuosp9yB6koBhGLT+X7BUQkN+5z2Ur/6ygpA6+Uzq/avJeExxRh80jOAbEzU5YsY6sFXgtPF2Zd9W6lZgFsOmW6i1hArZM84HMQQ7HtTalf9KJXS1JSrqCvxqVTSoR9HGRAU/6jDTC33ibRH+9yKa636hZuBIyZ7xMjKGR5wtohgQIbCWxm1XubirmCk5gnGaVv2Wy+jzLYgHxptRkDSv64oh60Fj2Royv3ueEDYpf/09ZKtDNKcRvJer5nqSgNPh1CRIs8L4xW8nHNyquee/nbJk8eNGkfOEYlAR8p6lICE1yWLyeWyURt8aMQoFqlpso8pYup+UBpSMxUPRgyWiCBkDubPfhBQGGL3kvZpadwNlazCHQLouFhziYofHCO2SR7FUQ0Nx/xbKn7iAwgevJH/+B0hf+iEONA1odxvFuK6mQsFTmqo0jzmN7LMvYODk38ccfpyYuE/0tHpeEUP/274g4YFdGm66i+rNP4A7r6Zkq1QCJcTM6m+cDIuhZELKhx3Pihf9lZS/+xE1P/tPKhja1Sb3Ygw4Rk8SUFLptscNlrJ6FHeto/xP55J9zccYf9zp5DbeQrULAlgMaSxpT2me9Hvkz3snqSc+R7rpCwOAl8JbeYx4K48hffqLCLc/oJVrv4Zc/3UKYZWK9VDbqWDTQUXwUMJMicJL3sP4Je9Rc91/UrGCYqdazSqIUUw6h/iZOZsnLUb0JgHzy5zma/eZDRmzhuLu9Yx/4U+Q406jKSmMBLO6yNR4lIylll2O98oPkT/rtSJeyn1ow0jSyexrSlWUOFtF8I56gpTeeBHBs/+fVr77YYoP3MC4ujSt2dwvKVEafUcQXvtVcptvYyR0yRbtrmyAMF+CTJd/JIsMPbkGNMUBl6XSwdVhsJQDQcoHMPddSxAEs6opNR4lL6R57NPo+/uryJ79JhEv5RIc4hQwMXMbNOL2lcN4rabn2BD/t58qpff9QILnX4iP4uFizG2HUKVugd0b8B6+ldHQIB3IJ1EHflNagaRyzNXBfzGipwgYU8hbvsa10p2tlhdLE6Gu0xpBTh/TeBRNSPOUcyi97/viH/07QhgQ7xvyiF5oREa1IeKnKb3mo5J902fwvBS+0LH5ZFy0VA1N1EC9w9xx0/Oi/tW92NC8pwgYuxu8NSeihQFnZc5CkDi1vRMshoKxNE9+LsULLxFTHHAv3PMPqSRp7ZBkQ3LnvEXSb/gknoDxzOzzn8tosYqqkjrxGYdsro81Fi8B21aAObVmVqyR9AlPJ+ubrmKubYfHkPeUxpFPoO/Cr4vJlRz5orSuQw4RN7YNyT/vzZJ60XspiD3oBAJnrFgqJot/wtMnrjHjxMVtIS9eAsaB9bZhL8g84/y2Pfi6gYq49meZAqW3fxGJJd+jRb7JME6tFl72AWmc/DzyxmIP5jWIIecL/vGn460+Qdo2MYo23l7MWJwEtCHh7o1RcsG0DI+ou3zqtHOluuJYsr66aMV8IIacUfxzL8Q/7inymJHPXdz98lIUX//P1FIFUmb2pcTMIaRlrOTPuxDx/Jnrv2hbsPDATrXjQ+7YIpSGi4+AasF4BDs3UPn2P6hGLpDWjuMiiFpMYRml89/v8v66yHxpDY8hayyVw08g/4ILhUm9+R4zRKrYW3OiZJ//DnJmfrl8Koaip3DSmaSf8gfSstQh0hruGTbuuFqr13zJ9bdepBby4iNgtM7LPOUPpLljA+WLXqbB6D4V46FxEkJ8zjMvkMYJZ1I0IdqlBBMjpAXy57wFyZUO/YuJCWDDiZ9OHQ1UyT3vzVTyy/E17EoKqgieKjWTofjKf2ByXDr+Y0UMtas+r6NfvpDcc16PZHKLNklh8REwhhiKb/oEjbU/p/6x82g+eIs6VaOAs/7w05TecBH1VNHFfOe4HRUhTUi5bzXpZ5zvHMcHQ754baq2VQLQIl1EAOcLjH7aXUMMoJjlayT91D8i50t3kth4FHwl/cJ3kTr+dMGGTnra0G03Wx6h/NU/18z3Pkj+hRfiHfFbE+csQizOWUUP1Fu+RnKv/Ri53esY/5eXUP3Jl1tJp4KgQQP/mN+R7Gv/mZQoxpPZCSWGtCekn3wOXv8q54nr9sXEBIsX9jHRxET+Qkc6Wx0l2PBrrf3qUq1d/19a//l3tNVao51BpUr2zFdSUQ+ZaxdN47PMC6k/4XkUXvp+wQYTEtx4BJvu1JF/eakWfvl1ho9+Gvlz37EwS4x5YPGG4qLU9+xZr5MDP7tEczvuofFf76Z573Waf+l78Y87TcS4mHDuuW+U8T2btPjjz1C2iivQbAOrWAPpk589IcXmEoBq3WhiWudqGKBj+1Vr42h5CNuoIV6KYMOtNK//OrWdD7PMDxj18uTOeSupEzr46aLoin/MKWJXHK2ZfZuo2w4JBxj6vIDqmlMp/enF4Kcmbmt4j9Zv+Aa1//0kfmOcYT9D4fz3g+c7J3inzlqLAIuYgM7wkHSO0gV/Q+Wzr6ahQuGuHzFy97Wkn3qeps+4gNRxp2JKK6T4qg9LOZNX+eG/ILFkmAQVwcdSSRUYePypE7Hd2TBJegQ71muw8Taaa28i3LmeYHArWh+DZrOlenOEqCpFI1R/65kMvO1ivMOPnbjIdOkc7y+X7ye15iRSBzZTU5lRTOeWDpb6bz+L0l/9j5jiAHZ0n4Z7NtO86xpqN36T7PA2ggDSKUGfch7pU/9AUOuk8yLG4iUgUQTBWtKnnSuVJ/yeFtfdQJk0Jqjh33op1Vt+yHhxJbJijXqlleB72FQaU2/OjCGokPIUlq/BW3lMxIRZG8O4LORta7X6/Y/SuPdnZJplUgJGcdJ0mhuy4vnkJKRx4pn0v/t/RHJ9aBjMGtJzHfs9vKOfgNx7dfvnoEooYBo1xr/yZ2qH9xAO7yHct4OSCfGsMo6HEaXpZSm86C+7fsYLjUVNQHAp5mI8Ci9+D2PrbkLDJhbDWLSrZWp8kFR5EC/efyPQtr5pMYIRwVtx9MS2W53Wi5FfsPqL72rtkneTq43QCJSKGoiu08qIbg3hDJywfzWlt34ByfU5w8Cb/RHHX/eP+K1Zz7MKmc23k9p6h9sCwipqYQz3R4qBoq80nvkKUsc+ZdGv/WIs/hmKAWtJnfL7Iic/l6LnVJLY0DUcUqESGsain06BEcXdrCkuj9R7hwV/RL7ajd/SxsV/gpSHGQkNIW7vYIlcK3GcuRVvVkgLpM97J96qx4uGwbyc26YwMGs3BYBKaBgJDRVraFjjWgvbEMRlAFVMjvwfvnVROpw7YdETcPJ+asUXXkhj2n6+ojqFGI8IkQO3ue6XWvvaXxKqUrcGExGu49dESBlLJTtA9hkviTJp5vloPR87h0009T4nJaeKoeCBf8YF+MecIsSbcPcAemKW8a6SqVPOEk54FgVPDz50NmtvF9Cx/Yx/9S/wmhUCPES68BWKIesJ6ZPPxKw4qn1cdq5pNRscrLlgbEhVPXLPfaMbK5GAhxYSt18TIXPmywHpmIzacQxcHVk4MthBQjnSVK74jOb2rqNqhRQBKXEJpIqZ09HtPf5JE2PNE7Z8AN8IOo+wIgDGI5cSzAln4B93qqg9+AybhUDvzDR6qJnTzqOWX0nKxKu67qBWCVUJ9m+daM07qXsqYmiuu1kbV36OWqiYXBFbXIUtrkLTeQq+kvU6RFusElrFGzhy3rcVU9XunKv50eyDZM98FeKlXPLtIoz5dsKit4JbEAEbYpYdLumnv0gzN/wHQ4HBdNv9IGqxIUO7Cfc8rP7jniRRTnErmBDsXE/mpX+Nf+IZmGVHIPk+F7IdP0DjodtoXP1FsjvXUgtnZlkrIJn8vG5JNYrEqBJsud/1Jp/H9y2GjFiq2QEGnnQ2B6P6Fxq9Q0BoBfDz513I8O1XUhjdRQUfdHYjAZyxYo1HwdZobrwD/5gntiIhsTrOPfcN7d//8jXkjnkimdNfwsinX6n59TdTVW+K0SMw7/YYEtWx2bH9BLs3uAxn277+YzrUeBhVskbRC/4Gb/nqnnG9TEZvzHaStQeKd8Rx0veu/yboO5J+L5xYoxkvyoppf1txDUVz7U3OFdNOVbUyWKz7RpR0oEEDUxyg8JL30Yzrcyd9XyRqej4f2ImWHt6BqKl6m/WjikzcW2R85SUkayzhee8m94dvm0q+xAg5xIhfdJzaFAakTni6FP/uaoJnvxayRfK+UvIsA76N1mptoJZaqDTvvhY7uNW97hm7pMcZLFEDuDj5IcrE8Y48niBTdBvRtL7jqtPCaIPp+d5X7cZvkfG0bR8bBXyUZb6l37PkjcUIBMedTvpd36Lwyg858kU1J72QBT0ZPaGC7eigmr6VMt314h9xnPhv/Xcy575Tm+t/RbhjA2HQINh8F6mHfj2jSaSo0hSPvvoQtZu+Rf78DyC2w16t7SDitu+aZqnGVWzBrg3dr8PijOVdD2njrmsIQmYkIbj4tWKLK6g/9YWIn8FbeTSlY59M+qRnCn4mmoCZcg+2PIKks0gq0+WNLRwWNQFVnUuhcftVVG/6pmZ+5zl4a07EW3MSZsVRYvJuH1//6JPFP/pkAML92zX8wlucqrUTTuwWrFITsNf9N7mz36z0HSbSzdopykO0g1ugOu62b4izY6zSRGluvRcd26fSt3LCspkNIlSu/DyFZplxDNKmfYiK4NsGmSefTfrpL50xoDbr2NFBDXeuJ9j2ADx8G2EqS+H1F4nrILG4paEsaqdl3GnAhoxc9Artf+haDlQV62Uxh63GW30CZmA1pu8w8HzC3Rup33Yl+WA8avrdHhZDX0oJz/oTim/+lGBDt6fcrF0PHEkr3/+Ymss/7grGJ0deRPAEiu/7AeknPU/iqEr7CbhwX7Dh1zr6kXOxgUueaLuvHLgECAE96UxSxz8NyeSxY8PYkd2EezYR7noIWxlleQZGbIr+D/0U/7jTesIoWdwEZEIKaq3MyMf+SNObbmM8ELLGkvLixAB3rghUArfF1aw1tSIYBC+dIv+X/0P6yefMUZjkYr1aHePA356lqb0PuR6Ak1Sm665gsc9/F4VXf2SWlx9FXCpjjH7sRWo23041nH2+sbAt+IIRp+7jVUBglZp1zno/nSV74SVkTju3J8gHPWCESFT/IdkCxQu/RuPIkyj6lhopxkLDaGgYs+5nNDRRz+U5fIOtdm51yl/+M8LdD2tcKNT29NDl+1Wuvlhzgw9RDWXGeg21NEKldsul2PEDQBv13yozFcb++wOa3noHVfXmnG8sl8ejeyxHv0dDQ1VTiAjGGNJv+YIjX7S+7AX0xiwjEvorHyfFd3+X5tGn0u9FPjfrAvStny4lumBp4OGP7GLs06/GDu9pS0KNUqrCbWu1ftXnqVtp2xJEosSF3NBW6r/4nisdmOQkdxspunspf/cj6v38G4xPV+NzzTm6R+LfQJomiE/uT79C7lkvf4xLTB85eoOAAOL21PUPf7yUPnApjd99KSUTkhKd1fc3K2xINTR4O+9n7KKXEUSSUKPeMHFGsa2OMnrxn5Kujcy5pULTQvWqf8OODmr8h9MqClIof+vv1F7+CSrhpFLTeaDlExShz7OEy4+i9L7vkX3my0V7jHzQA2vAGZi0tqlefbFWf/hxCrUhqqG6VC1j3CJJZu8LM2VI41EwIc3ljyf/x58l/aSzndZTRW3I2L++Xv07fsS4nVtixQZO8IxXUHrHV1rWcDgyqJX//AvMHVc48nXoeNV2fnEBFOBpSM4T6qGSOuMCcq/6R7zDjhaNquJ6Db1HQIisY7fwDnc/rPXrL6F28w/w928hbYSmVWohE7XCauckoxqPrIQ0SJE/7x1kX/weMfl+xr/6LvVu/BrjtvMacSYMWc9iXvwBChf8tdRvuUxr3/sI6b0bGQukqz8OxWVfY5W0saQ9R9eKyZF+4tlkz36jq/uAx66tyKOA3iQgtFRka4+48giNtTdo494bCNf9ksb2++mLvJzVQGl2UwCH67/clxbKR56CN7Aac+81VDtUqnUeBzzjUsb8459G+NCtpI1Q1e7WfALkI4u3YZVm6XC8E04n84QzST3xufhrTpyQ0PMpLV2E6F0CxojDUJMkgG3UCB++Q5trf06wfS2N+27EjA86C3kuyROpu6yEeCKu0eVBNAWPXSc5D6qhI3fXPaKNR/bkM/HWnEjqxDNInXwmpn/VxN+PqlPhPSr1JqP3CRgjThqAKS/Gjg8x8sHfU9m/xUVGupRkinEht0eY5u+2eeh+PYoYMp6Q/uPPkz3rdVNIN1EU37sSbzqWzp1E3QEk2lGdsIkGDcYufptmh7bQUKdGNcqCUZHWVgxth8M+8hqTaJxO5IuzrOP5IM5mCcOQyrf+mmDTnTql5UcrSWLpYGndzWR4KSrXfFH9e65m3PotEngoPkpalJxnp2a1PMbIeZaMsaSivtGiznpviE+6MsLYf/wFxNV1S0VTTcPSUcExIjdN/bYr1X7uVQA01WUfNyzgZ5F0GpPOo/2rsIOb8aujc26vekghLusvdcwpboPr8hA0GtigTooQPwq39WeE0adcQN/bLxb1Uz1tbHTCos6GORgogoQBdvdD2D98J7JiDenCAFJYRi7fh8n3I/l+TDqL5PulcddPtPzZ1+BhsF1uZvOI5mc8+jxL/akvpe/tXxSadbReUa2OYctDaHUMrYyi5WGqo4OYod3Y4T1qVh7TXYZNj2HpScCDwNhX/lxTN32N0dCb1U0S76LUCZFTrvP3MWQ8JRw4iv4P/xRv+ZqlxaaDwNIlYOyemXwIJqVcSUtda2WEob9/rqb2bJg1MyUV+Ybb9puM/tMI2xdlqghGDClCsn/2NTLPvGBS0sCkb7QbfAm4Wzph6RKwW0QkbNx7nZY/eQG2GRAy020iAMuOxKTSzgc3TRIqgBGC/TvwbDCDhLHqbTzrdfS97Qs9lzTwaCEhILRCWWPf+KCmfvL5GcmmcazYnvlGSm/+lNhmfapBoBY1PsF912v5X19H0GxMkWRxX+pg5bH0f/havL6Vjr1LbD13MFh6ZtXBIMpaKb7sA1I56skUJJxagK6WWig0bvwmwcN3qsmVkGxh4idXwqQylK/8HBnbwE6WjuJCagGGwhs/hde/SrTHCoceTSQEhBYZJFei9IaLaJis20orPq6KFUNOAsa/91GwAWpDVG2rcXr1hm9oesMvGQ9kyn6+KoaCr6TOeRuZU13m9VIIoR0qJASMEeUbpk48Q1Ivfjd5f1p1mw0pW4O/7jpqP/+OivEQaxHPQ8f2U7nsIoJpyxmLISch1VUnUXrF3/ZU16rHCsnTmIQ4/b/wwndJ5binUzB2yvYPGnVFLf/vJ10RetTmt3zFZzR/YCv1Sen1KoIv0DRpSm/5LOT6WIp+vEeKhICTEavcTI7+N3yCRiqH32pt6eK6NfUoDG6kctW/KcYj2Hq/Nq79KlXLlBR8xJDzlPQL3kXqpGf2VJ3GY4nkiUxHvEXEsadK5oK/J2smlaDh1oOVEGo/+RLhzvVaufzTFIIKTTvRVsNtAWupP+53KZ7//p6pUFsIJG6Ydog3wwkDRj/xCk2t/T/GJhkXca2uPeJ49MAOpFnFRv0KneoVJJun9LdX4h/bG/W5C4XkqbRDrIr9NMU3f4padhlpb6JTqgBNBG/PBqhXWuRzHxqyRkm/+L2OfInqnRXJk+mEWBUffpzkXv1PpGCK81lUaVozJeKhxqNkLI0nnEXhBX+eSL4ukDyd2RBZxbmzXifhU15AcZpVzBR/n5BCqeWWUXrdx92u65BYvXMgIeBsiMkjQvFNn6Lat5qMtG/TK2JIGyVzwd/hHX1yonq7RPKE5kJr48TVkn/1RxB1myJOho2s3uDUF5B73ptdgXhCvq6QPKVuEEVJss96ufDsV09RxYohLZZq/jBKr/0oYjyX8pWo3q6QELAbxIRSpfjqj1BdfixZ3CbZxggeUHjdP2MOPy6RfvNE8qS6hEQ7k5v+VVJ4/cejrbsMBWOxz3g5mWe/omfbYywkEgLOAxJ1z0qfdq7457yNlemAysDR9L3mo9HuRInanS8SAs4X0b4exZd/UEaOfBLFV/0jZuCIaG/e5HHOF0ko7hHAju5TU1jmmqcnRsdBISHgwSJJrTokSHTGwUI6lMclmBcSAj4SJBLwESMhYIIFRULABAuKhIAJFhQJARMsKBICJlhQJARMsKBICJhgQZEQMMGCIiFgggVFQsAEC4qEgAkWFAkBEywoEgImWFAkBEywoEgImGBBkRAwwYIiIWCCBUVCwAQLioSACRYU/x94CTKJ8X+Z5wAAAABJRU5ErkJggg==" alt="Fenix logo" width="40" height="40"></span>
|
|
115
|
+
<div>
|
|
116
|
+
<h1>Practice Dashboard</h1>
|
|
117
|
+
<div class="sub" id="sub">Live · loading…</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="acts">
|
|
121
|
+
<button class="btn" id="aiBtn">✦ Priority briefing</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="prog" id="prog"></div>
|
|
126
|
+
|
|
127
|
+
<div class="kpis" id="kpis"></div>
|
|
128
|
+
|
|
129
|
+
<div class="tabs" id="tabs">
|
|
130
|
+
<button data-t="overview" class="on">Overview</button>
|
|
131
|
+
<button data-t="deadlines">Deadlines</button>
|
|
132
|
+
<button data-t="tasks">Open tasks</button>
|
|
133
|
+
<button data-t="team">Team</button>
|
|
134
|
+
<button data-t="clients">Clients</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="bar" id="filterbar" style="display:none">
|
|
138
|
+
<select id="fClient"><option value="">All clients</option></select>
|
|
139
|
+
<select id="fAssignee"><option value="">All assignees</option></select>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="ai" id="aiBox">
|
|
143
|
+
<div class="hd"><span class="dot"></span> Priority briefing</div>
|
|
144
|
+
<div class="bd" id="aiBd"></div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div id="view"><div class="center"><div class="spin"></div>Pulling data from Fenix…</div></div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
const SRV='mcp__25de2133-8f0a-47c4-b66d-b520d6be5fe7__';
|
|
152
|
+
const BASE='https://chau.fenix.ai/dashboard/projects/';
|
|
153
|
+
let PROJECTS=[],TASKS=[],CLIENTS=[],USERS=[];
|
|
154
|
+
let TAB='overview', Q='', FC='', FA='';
|
|
155
|
+
let charts={}, grid=null;
|
|
156
|
+
let TASK_MODE='full', TASK_PROG='';
|
|
157
|
+
const LOAD={projects:'load',tasks:'load',clients:'load',users:'load'};
|
|
158
|
+
const LABELS={projects:'Active projects',tasks:'Open tasks',clients:'Clients',users:'Team'};
|
|
159
|
+
|
|
160
|
+
function unwrap(res){
|
|
161
|
+
if(res==null) return null;
|
|
162
|
+
if(typeof res==='string'){ try{return JSON.parse(res)}catch(e){return res} }
|
|
163
|
+
if(Array.isArray(res)) return res;
|
|
164
|
+
if(Array.isArray(res.content)){ const t=res.content.map(c=>c&&c.text||'').join(''); try{return JSON.parse(t)}catch(e){return res} }
|
|
165
|
+
if(res.result!==undefined) return unwrap(res.result);
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
function asArray(res,key){ const u=unwrap(res); if(Array.isArray(u))return u; if(u&&Array.isArray(u[key]))return u[key]; return []; }
|
|
169
|
+
function call(tool){ return window.cowork.callMcpTool(SRV+tool,{}).catch(()=>null); }
|
|
170
|
+
|
|
171
|
+
const today=(()=>{const n=new Date();return new Date(n.getFullYear(),n.getMonth(),n.getDate())})();
|
|
172
|
+
function pd(raw){
|
|
173
|
+
if(!raw) return null;
|
|
174
|
+
let s=String(raw).trim(); if(!s) return null;
|
|
175
|
+
s=s.replace(/(\d+)(st|nd|rd|th)/gi,'$1');
|
|
176
|
+
let iso=s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
|
177
|
+
if(iso) return new Date(+iso[1],+iso[2]-1,+iso[3]);
|
|
178
|
+
let mdy=s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/);
|
|
179
|
+
if(mdy){ let mo=+mdy[1],d=+mdy[2],y=+mdy[3]; if(y<100)y+=2000; return new Date(y,mo-1,d); }
|
|
180
|
+
let t=Date.parse(s); if(isNaN(t)) return null;
|
|
181
|
+
const dt=new Date(t); return new Date(dt.getFullYear(),dt.getMonth(),dt.getDate());
|
|
182
|
+
}
|
|
183
|
+
function dleft(d){ return d?Math.round((d-today)/86400000):null; }
|
|
184
|
+
function fmt(d){ return d?d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):'—'; }
|
|
185
|
+
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
|
186
|
+
function clientOf(c){ return c||'Unassigned'; }
|
|
187
|
+
function wa2flag(w){ return w&&typeof w==='object'&&Object.keys(w).length>0; }
|
|
188
|
+
|
|
189
|
+
function urg(dl){
|
|
190
|
+
if(dl===null) return {bg:'var(--graybg)',fg:'var(--gray)',lab:'No date'};
|
|
191
|
+
if(dl<0) return {bg:'var(--redbg)',fg:'var(--red)',lab:Math.abs(dl)+'d overdue'};
|
|
192
|
+
if(dl<=7) return {bg:'var(--redbg)',fg:'var(--red)',lab:dl+'d left'};
|
|
193
|
+
if(dl<=21) return {bg:'var(--orangebg)',fg:'var(--orange)',lab:dl+'d left'};
|
|
194
|
+
if(dl<=45) return {bg:'var(--amberbg)',fg:'var(--amber)',lab:dl+'d left'};
|
|
195
|
+
return {bg:'var(--greenbg)',fg:'var(--green)',lab:dl+'d left'};
|
|
196
|
+
}
|
|
197
|
+
function dueCell(ts){
|
|
198
|
+
const d = ts && ts<8e15 ? new Date(ts) : null;
|
|
199
|
+
const dl = d?dleft(d):null, u=urg(dl);
|
|
200
|
+
return `<span class="due mono">${fmt(d)}</span><span class="days" style="background:${u.bg};color:${u.fg}">${u.lab}</span>`;
|
|
201
|
+
}
|
|
202
|
+
function statusPill(s){
|
|
203
|
+
const l=(s||'').toLowerCase();
|
|
204
|
+
let bg='var(--graybg)',fg='var(--gray)',t=s||'—';
|
|
205
|
+
if(l==='active'){bg='var(--accentbg)';fg='var(--accent)';t='Active';}
|
|
206
|
+
else if(l==='in-progress'){bg='var(--amberbg)';fg='var(--amber)';t='In progress';}
|
|
207
|
+
else if(l==='completed'){bg='var(--greenbg)';fg='var(--green)';t='Completed';}
|
|
208
|
+
return `<span class="pill" style="background:${bg};color:${fg}">${esc(t)}</span>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function mapProjects(ap){
|
|
212
|
+
PROJECTS=asArray(ap,'projects').map(p=>{const d=pd(p.due_date);return {
|
|
213
|
+
projectId:p.projectId, matterNum:p.matterNum||'(no matter #)', type:p.project_type||'',
|
|
214
|
+
client:clientOf(p.clientName), status:p.status||'', wa2:wa2flag(p.wa2),
|
|
215
|
+
url:BASE+p.projectId, _d:d, _dl:dleft(d)
|
|
216
|
+
};});
|
|
217
|
+
}
|
|
218
|
+
function mapTasks(ot){
|
|
219
|
+
const pc={}; PROJECTS.forEach(p=>{pc[p.projectId]=p.client;});
|
|
220
|
+
TASKS=asArray(ot,'tasks').map(t=>{const d=pd(t.dueDate);return {
|
|
221
|
+
name:t.taskName||'(task)', matterNum:t.matterNum||'', client:pc[t.projectId]||'Unassigned',
|
|
222
|
+
assignee:(t.assignee&&t.assignee.initials)||'', assigneeName:(t.assignee&&t.assignee.name)||'',
|
|
223
|
+
projectId:t.projectId, url:BASE+t.projectId, _d:d, _dl:dleft(d)
|
|
224
|
+
};});
|
|
225
|
+
}
|
|
226
|
+
function mapClients(cl){ CLIENTS=asArray(cl,'clients').map(c=>({client:clientOf(c.clientName),count:c.activeProjectCount||0})); }
|
|
227
|
+
function mapUsers(us){
|
|
228
|
+
USERS=asArray(us,'users').map(u=>({
|
|
229
|
+
initials:u.initials||(u.name||'?').split(' ').map(x=>x[0]).join('').slice(0,3).toUpperCase(),
|
|
230
|
+
name:u.name||'Unknown', hours:u.totalEstimatedHours||0, projects:u.totalActiveProjects||0
|
|
231
|
+
})).filter(u=>u.projects>0||u.hours>0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderProg(){
|
|
235
|
+
const allDone=Object.values(LOAD).every(s=>s==='done'||s==='err');
|
|
236
|
+
if(allDone){ document.getElementById('prog').innerHTML=''; return; }
|
|
237
|
+
document.getElementById('prog').innerHTML=Object.keys(LOAD).map(k=>{
|
|
238
|
+
const s=LOAD[k];
|
|
239
|
+
const ico=s==='done'?'✓':s==='err'?'✕':'<span class="sp"></span>';
|
|
240
|
+
const cls=s==='done'?'chip done':s==='err'?'chip err':'chip';
|
|
241
|
+
const txt=s==='done'?LABELS[k]+' loaded':s==='err'?LABELS[k]+' failed':(k==='tasks'&&TASK_PROG?'Loading open tasks '+TASK_PROG+'…':'Loading '+LABELS[k].toLowerCase()+'…');
|
|
242
|
+
return `<span class="${cls}">${ico} ${txt}</span>`;
|
|
243
|
+
}).join('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function load(){
|
|
247
|
+
['projects','tasks','clients','users'].forEach(k=>LOAD[k]='load');
|
|
248
|
+
renderProg(); render();
|
|
249
|
+
const step=(key,fn,res)=>{ try{fn(res);LOAD[key]='done';}catch(e){LOAD[key]='err';} buildFilters(); renderProg(); render(); };
|
|
250
|
+
call('list_clients').then(r=>step('clients',mapClients,r)).catch(()=>{LOAD.clients='err';renderProg();render();});
|
|
251
|
+
call('list_users').then(r=>step('users',mapUsers,r)).catch(()=>{LOAD.users='err';renderProg();render();});
|
|
252
|
+
// Tasks load after projects so the fallback can enrich each task with its project's matter/client.
|
|
253
|
+
call('list_active_projects').then(r=>{step('projects',mapProjects,r); loadTasks();}).catch(()=>{LOAD.projects='err';renderProg();render();loadTasks();});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function loadTasks(){
|
|
257
|
+
LOAD.tasks='load'; TASK_PROG=''; renderProg();
|
|
258
|
+
// 1) Try the firm-wide feed (rich: includes assignee). May fail/return empty if too large for the channel.
|
|
259
|
+
let r=null; try{ r=await call('list_open_tasks'); }catch(e){}
|
|
260
|
+
const bulk = r==null?[]:asArray(r,'tasks');
|
|
261
|
+
if(bulk.length){
|
|
262
|
+
try{ mapTasks(r); TASK_MODE='full'; LOAD.tasks='done'; }catch(e){ LOAD.tasks='err'; }
|
|
263
|
+
buildFilters(); renderProg(); render(); return;
|
|
264
|
+
}
|
|
265
|
+
// 2) Fallback: fetch open tasks per active project (lighter calls; no assignee field available).
|
|
266
|
+
TASK_MODE='lite';
|
|
267
|
+
const projs=PROJECTS.slice(); const out=[]; let i=0, done=0; const N=8;
|
|
268
|
+
async function worker(){
|
|
269
|
+
while(i<projs.length){
|
|
270
|
+
const p=projs[i++];
|
|
271
|
+
try{
|
|
272
|
+
const pr=await window.cowork.callMcpTool(SRV+'list_project_tasks',{projectId:p.projectId,includeCompleted:false});
|
|
273
|
+
asArray(pr,'tasks').forEach(t=>{
|
|
274
|
+
const st=(t.status||'').toLowerCase();
|
|
275
|
+
if(st==='completed'||st==='complete'||st==='done') return;
|
|
276
|
+
const d=pd(t.dueDate);
|
|
277
|
+
out.push({ name:t.name||'(task)', matterNum:p.matterNum, client:p.client,
|
|
278
|
+
assignee:'', assigneeName:'', milestone:t.milestone||'',
|
|
279
|
+
projectId:p.projectId, url:BASE+p.projectId, _d:d, _dl:dleft(d) });
|
|
280
|
+
});
|
|
281
|
+
}catch(e){}
|
|
282
|
+
done++; TASK_PROG=done+'/'+projs.length; renderProg();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
try{ await Promise.all(Array.from({length:Math.min(N,projs.length||1)},worker)); TASKS=out; LOAD.tasks='done'; }
|
|
286
|
+
catch(e){ LOAD.tasks='err'; }
|
|
287
|
+
TASK_PROG=''; buildFilters(); renderProg(); render();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildFilters(){
|
|
291
|
+
const cs=[...new Set(PROJECTS.map(p=>p.client))].sort();
|
|
292
|
+
const as=[...new Set(TASKS.map(t=>t.assignee).filter(Boolean))].sort();
|
|
293
|
+
const fc=document.getElementById('fClient'), fa=document.getElementById('fAssignee');
|
|
294
|
+
fc.innerHTML='<option value="">All clients</option>'+cs.map(c=>`<option ${c===FC?'selected':''}>${esc(c)}</option>`).join('');
|
|
295
|
+
fa.innerHTML='<option value="">All assignees</option>'+as.map(a=>`<option ${a===FA?'selected':''}>${esc(a)}</option>`).join('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function render(){
|
|
299
|
+
const odP=PROJECTS.filter(p=>p._dl!==null&&p._dl<0).length;
|
|
300
|
+
const odT=TASKS.filter(t=>t._dl!==null&&t._dl<0).length;
|
|
301
|
+
const wa=PROJECTS.filter(p=>p.wa2).length;
|
|
302
|
+
const P=LOAD.projects==='done',T=LOAD.tasks==='done',C=LOAD.clients==='done',U=LOAD.users==='done';
|
|
303
|
+
const v=(ok,n)=>ok?n:'<span class="ld">…</span>';
|
|
304
|
+
document.getElementById('kpis').innerHTML=`
|
|
305
|
+
<div class="kpi accent"><div class="n">${v(P,PROJECTS.length)}</div><div class="l">Active projects</div></div>
|
|
306
|
+
<div class="kpi"><div class="n">${v(T,TASKS.length)}</div><div class="l">Open tasks</div></div>
|
|
307
|
+
<div class="kpi red"><div class="n">${v(P,odP)}</div><div class="l">Overdue projects</div></div>
|
|
308
|
+
<div class="kpi orange"><div class="n">${v(T,odT)}</div><div class="l">Overdue tasks</div></div>
|
|
309
|
+
<div class="kpi purple"><div class="n">${v(P,wa)}</div><div class="l">Attorney flags</div></div>
|
|
310
|
+
<div class="kpi"><div class="n">${v(C,CLIENTS.length)}</div><div class="l">Clients</div></div>`;
|
|
311
|
+
const allDone=Object.values(LOAD).every(s=>s==='done'||s==='err');
|
|
312
|
+
document.getElementById('sub').textContent='Live · '+(P?PROJECTS.length:'…')+' active projects · '+(T?TASKS.length:'…')+' open tasks · '+(allDone?'updated '+new Date().toLocaleString():'loading…');
|
|
313
|
+
document.getElementById('filterbar').style.display=(TAB==='deadlines'||TAB==='tasks')?'flex':'none';
|
|
314
|
+
document.getElementById('fAssignee').style.display=(TAB==='tasks'&&TASK_MODE==='full')?'':'none';
|
|
315
|
+
Object.values(charts).forEach(c=>{try{c.destroy()}catch(e){}}); charts={}; grid=null;
|
|
316
|
+
const view=document.getElementById('view');
|
|
317
|
+
const loadingBox=m=>`<div class="center"><div class="spin"></div>${m}</div>`;
|
|
318
|
+
if(TAB==='overview'){ view.innerHTML=overview(); if(C)drawClientChartOv(); if(U)drawHoursChartOv(); }
|
|
319
|
+
else if(TAB==='deadlines'){ if(P){view.innerHTML='<div id="gw"></div>';buildDeadlineGrid();} else view.innerHTML=loadingBox('Loading active projects…'); }
|
|
320
|
+
else if(TAB==='tasks'){ if(T){view.innerHTML='<div id="gw"></div>';buildTaskGrid();} else view.innerHTML=loadingBox('Loading open tasks — this is the largest dataset…'); }
|
|
321
|
+
else if(TAB==='team'){ view.innerHTML = U?team():loadingBox('Loading team…'); if(U)drawTeamChart(); }
|
|
322
|
+
else if(TAB==='clients'){ view.innerHTML = C?clientsView():loadingBox('Loading clients…'); if(C)drawClientChart(); }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function overview(){
|
|
326
|
+
const P=LOAD.projects==='done',C=LOAD.clients==='done',U=LOAD.users==='done';
|
|
327
|
+
const mini='<div class="center" style="padding:80px 0"><div class="spin"></div></div>';
|
|
328
|
+
const attn=PROJECTS.filter(p=>(p._dl!==null&&p._dl<=14)||p.wa2).sort((a,b)=>(a._d?a._d:1e15)-(b._d?b._d:1e15)).slice(0,10);
|
|
329
|
+
const rows=attn.map(p=>`<tr>
|
|
330
|
+
<td>${dueCell(p._d?p._d.getTime():9e15)}</td>
|
|
331
|
+
<td><div class="title">${esc(p.matterNum)}${p.wa2?'<span class="flag">⚑ Attorney</span>':''}</div><div class="meta">${esc(p.type||'—')} · ${esc(p.client)}</div></td>
|
|
332
|
+
<td style="text-align:right"><a class="lk" href="${p.url}" target="_blank" rel="noopener">Open ↗</a></td></tr>`).join('');
|
|
333
|
+
return `<div class="grid2">
|
|
334
|
+
<div class="panel"><h3>Active projects by client</h3>${C?'<canvas id="cClient" role="img" aria-label="Active projects by client"></canvas>':mini}</div>
|
|
335
|
+
<div class="panel"><h3>Team assigned hours (top 12)</h3>${U?'<canvas id="cHours" role="img" aria-label="Assigned hours by team member"></canvas>':mini}</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="panel"><h3>Needs attention — due ≤14 days or Attorney flagged</h3>
|
|
338
|
+
${!P?mini:(attn.length?'<table class="atable">'+rows+'</table>':'<div class="typ">Nothing flagged.</div>')}
|
|
339
|
+
</div>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function gReady(){ return typeof gridjs!=='undefined'; }
|
|
343
|
+
function buildDeadlineGrid(){
|
|
344
|
+
if(!gReady()){ document.getElementById('gw').innerHTML='<div class="center">Table library loading…</div>'; setTimeout(()=>{if(TAB==='deadlines')buildDeadlineGrid();},300); return; }
|
|
345
|
+
let data=PROJECTS.filter(p=>!FC||p.client===FC).map(p=>[
|
|
346
|
+
p._d?p._d.getTime():9e15, p.matterNum, p.wa2?1:0, p.client, p.type||'—', p.status, p.url
|
|
347
|
+
]);
|
|
348
|
+
grid=new gridjs.Grid({
|
|
349
|
+
columns:[
|
|
350
|
+
{name:'Due',formatter:c=>gridjs.html(dueCell(c))},
|
|
351
|
+
{name:'Matter',formatter:(c,r)=>gridjs.html('<span class="title">'+esc(c)+'</span>'+(r.cells[2].data?'<span class="flag">⚑ Attorney</span>':''))},
|
|
352
|
+
{name:'Attorney',hidden:true},
|
|
353
|
+
{name:'Client'},
|
|
354
|
+
{name:'Type',formatter:c=>gridjs.html('<span class="typ">'+esc(c)+'</span>')},
|
|
355
|
+
{name:'Status',formatter:c=>gridjs.html(statusPill(c))},
|
|
356
|
+
{name:'',sort:false,formatter:c=>gridjs.html('<a class="lk" href="'+c+'" target="_blank" rel="noopener">Open ↗</a>')}
|
|
357
|
+
],
|
|
358
|
+
data, search:true, sort:true, resizable:true,
|
|
359
|
+
pagination:{limit:25,summary:true},
|
|
360
|
+
language:{search:{placeholder:'Search matter, client, type…'}}
|
|
361
|
+
});
|
|
362
|
+
grid.render(document.getElementById('gw'));
|
|
363
|
+
}
|
|
364
|
+
function buildTaskGrid(){
|
|
365
|
+
if(!gReady()){ document.getElementById('gw').innerHTML='<div class="center">Table library loading…</div>'; setTimeout(()=>{if(TAB==='tasks')buildTaskGrid();},300); return; }
|
|
366
|
+
let data=TASKS.filter(t=>(!FA||t.assignee===FA)&&(!FC||t.client===FC)).map(t=>[
|
|
367
|
+
t._d?t._d.getTime():9e15, t.name, t.matterNum, t.client, t.assignee, t.assigneeName, t.url
|
|
368
|
+
]);
|
|
369
|
+
grid=new gridjs.Grid({
|
|
370
|
+
columns:[
|
|
371
|
+
{name:'Due',formatter:c=>gridjs.html(dueCell(c))},
|
|
372
|
+
{name:'Task',formatter:c=>gridjs.html('<span class="title">'+esc(c)+'</span>')},
|
|
373
|
+
{name:'Matter'},
|
|
374
|
+
{name:'Client',formatter:c=>gridjs.html('<span class="typ">'+esc(c)+'</span>')},
|
|
375
|
+
{name:'Assignee',formatter:(c,r)=>gridjs.html(esc(c||'—')+(r.cells[5].data?'<div class="meta">'+esc(r.cells[5].data)+'</div>':''))},
|
|
376
|
+
{name:'_n',hidden:true},
|
|
377
|
+
{name:'',sort:false,formatter:c=>gridjs.html('<a class="lk" href="'+c+'" target="_blank" rel="noopener">Open ↗</a>')}
|
|
378
|
+
],
|
|
379
|
+
data, search:true, sort:true, resizable:true,
|
|
380
|
+
pagination:{limit:30,summary:true},
|
|
381
|
+
language:{search:{placeholder:'Search task, matter, assignee…'}}
|
|
382
|
+
});
|
|
383
|
+
grid.render(document.getElementById('gw'));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function team(){
|
|
387
|
+
const us=[...USERS].sort((a,b)=>b.hours-a.hours);
|
|
388
|
+
const maxH=Math.max(1,...us.map(u=>u.hours));
|
|
389
|
+
const cards=us.map(u=>{
|
|
390
|
+
const ot=TASKS.filter(t=>t.assignee===u.initials).length;
|
|
391
|
+
const od=TASKS.filter(t=>t.assignee===u.initials&&t._dl!==null&&t._dl<0).length;
|
|
392
|
+
const w=Math.round(u.hours/maxH*100);
|
|
393
|
+
return `<div class="ucard"><div class="av">${esc(u.initials)}</div>
|
|
394
|
+
<div class="grow"><div style="font-weight:700">${esc(u.name)}</div>
|
|
395
|
+
<div class="meta">${u.projects} active projects · ${ot} open tasks${od?` · <span style="color:var(--red);font-weight:700">${od} overdue</span>`:''}</div>
|
|
396
|
+
<div class="hbar"><i style="width:${w}%"></i></div></div>
|
|
397
|
+
<div style="text-align:right"><div class="num mono">${u.hours}h</div><div class="meta">assigned</div></div></div>`;
|
|
398
|
+
}).join('');
|
|
399
|
+
return `<div class="panel" style="margin-bottom:14px"><h3>Assigned hours by person</h3><canvas id="cTeam" role="img" aria-label="Assigned hours by team member"></canvas></div>${cards}`;
|
|
400
|
+
}
|
|
401
|
+
function clientsView(){
|
|
402
|
+
const rows=[...CLIENTS].sort((a,b)=>b.count-a.count).map(c=>{
|
|
403
|
+
const ps=PROJECTS.filter(p=>p.client===c.client);
|
|
404
|
+
const od=ps.filter(p=>p._dl!==null&&p._dl<0).length;
|
|
405
|
+
const soon=ps.filter(p=>p._dl!==null&&p._dl>=0&&p._dl<=30).length;
|
|
406
|
+
const wa=ps.filter(p=>p.wa2).length;
|
|
407
|
+
return `<tr>
|
|
408
|
+
<td><div class="title">${esc(c.client)}</div></td>
|
|
409
|
+
<td><span class="num mono">${c.count}</span></td>
|
|
410
|
+
<td>${soon}</td>
|
|
411
|
+
<td>${od?`<span style="color:var(--red);font-weight:700">${od}</span>`:'0'}</td>
|
|
412
|
+
<td>${wa?`<span style="color:var(--purple);font-weight:700">${wa}</span>`:'0'}</td></tr>`;
|
|
413
|
+
}).join('');
|
|
414
|
+
return `<div class="panel" style="margin-bottom:14px"><h3>Active projects by client</h3><canvas id="cClient2" role="img" aria-label="Active projects by client"></canvas></div>
|
|
415
|
+
<div class="panel"><table class="atable">
|
|
416
|
+
<tr><td class="typ" style="font-size:11px;text-transform:uppercase;letter-spacing:.04em">Client</td><td class="typ" style="font-size:11px;text-transform:uppercase">Active</td><td class="typ" style="font-size:11px;text-transform:uppercase">Due ≤30d</td><td class="typ" style="font-size:11px;text-transform:uppercase">Overdue</td><td class="typ" style="font-size:11px;text-transform:uppercase">Attorney</td></tr>
|
|
417
|
+
${rows}</table></div>`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const PAL=['#818cf8','#a78bfa','#34d399','#fb923c','#22d3ee','#f472b6','#60a5fa','#fbbf24'];
|
|
421
|
+
function chartBase(){ if(typeof Chart!=='undefined'){ Chart.defaults.color='#8b92a6'; Chart.defaults.borderColor='rgba(255,255,255,0.05)'; Chart.defaults.font.family='-apple-system,Segoe UI,Roboto,sans-serif'; } }
|
|
422
|
+
function drawClientChartOv(){
|
|
423
|
+
if(typeof Chart==='undefined')return; chartBase();
|
|
424
|
+
const cs=[...CLIENTS].sort((a,b)=>b.count-a.count); const el=document.getElementById('cClient'); if(!el)return;
|
|
425
|
+
charts.c1=new Chart(el,{type:'doughnut',data:{labels:cs.map(c=>c.client),datasets:[{data:cs.map(c=>c.count),backgroundColor:PAL,borderColor:'#14171f',borderWidth:2}]},options:{plugins:{legend:{position:'right',labels:{usePointStyle:true,pointStyle:'rectRounded'}}},cutout:'64%'}});
|
|
426
|
+
}
|
|
427
|
+
function drawHoursChartOv(){
|
|
428
|
+
if(typeof Chart==='undefined')return; chartBase();
|
|
429
|
+
const us=[...USERS].sort((a,b)=>b.hours-a.hours).slice(0,12); const el=document.getElementById('cHours'); if(!el)return;
|
|
430
|
+
charts.c2=new Chart(el,{type:'bar',data:{labels:us.map(u=>u.initials),datasets:[{data:us.map(u=>u.hours),backgroundColor:'#818cf8',borderRadius:6}]},options:{plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,grid:{color:'rgba(255,255,255,0.05)'}},x:{grid:{display:false}}}}});
|
|
431
|
+
}
|
|
432
|
+
function drawTeamChart(){
|
|
433
|
+
if(typeof Chart==='undefined')return; chartBase();
|
|
434
|
+
const us=[...USERS].sort((a,b)=>b.hours-a.hours); const el=document.getElementById('cTeam'); if(!el)return;
|
|
435
|
+
charts.t=new Chart(el,{type:'bar',data:{labels:us.map(u=>u.name),datasets:[{data:us.map(u=>u.hours),backgroundColor:'#a78bfa',borderRadius:6}]},options:{indexAxis:'y',plugins:{legend:{display:false}},scales:{x:{beginAtZero:true,grid:{color:'rgba(255,255,255,0.05)'}},y:{grid:{display:false}}}}});
|
|
436
|
+
}
|
|
437
|
+
function drawClientChart(){
|
|
438
|
+
if(typeof Chart==='undefined')return; chartBase();
|
|
439
|
+
const cs=[...CLIENTS].sort((a,b)=>b.count-a.count); const el=document.getElementById('cClient2'); if(!el)return;
|
|
440
|
+
charts.cc=new Chart(el,{type:'bar',data:{labels:cs.map(c=>c.client),datasets:[{data:cs.map(c=>c.count),backgroundColor:PAL,borderRadius:6}]},options:{plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,grid:{color:'rgba(255,255,255,0.05)'}},x:{grid:{display:false}}}}});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
document.getElementById('tabs').addEventListener('click',e=>{
|
|
444
|
+
const b=e.target.closest('button'); if(!b)return;
|
|
445
|
+
TAB=b.dataset.t;
|
|
446
|
+
[...e.currentTarget.children].forEach(x=>x.classList.toggle('on',x===b));
|
|
447
|
+
render();
|
|
448
|
+
});
|
|
449
|
+
document.getElementById('fClient').addEventListener('change',e=>{FC=e.target.value;render()});
|
|
450
|
+
document.getElementById('fAssignee').addEventListener('change',e=>{FA=e.target.value;render()});
|
|
451
|
+
|
|
452
|
+
document.getElementById('aiBtn').addEventListener('click',async()=>{
|
|
453
|
+
const box=document.getElementById('aiBox'),bd=document.getElementById('aiBd');
|
|
454
|
+
box.classList.add('show'); bd.textContent='Analyzing deadlines and workload…';
|
|
455
|
+
const pj=PROJECTS.filter(p=>p._d&&p._dl<=30).sort((a,b)=>a._d-b._d).map(p=>({matter:p.matterNum,type:p.type,client:p.client,due:fmt(p._d),daysLeft:p._dl,wa2:p.wa2,status:p.status}));
|
|
456
|
+
const tk=TASKS.filter(t=>t._d&&t._dl<=14).sort((a,b)=>a._d-b._d).map(t=>({matter:t.matterNum,task:t.name,assignee:t.assignee,due:fmt(t._d),daysLeft:t._dl}));
|
|
457
|
+
const wl=[...USERS].sort((a,b)=>b.hours-a.hours).slice(0,12).map(u=>({person:u.name,initials:u.initials,hours:u.hours,projects:u.projects}));
|
|
458
|
+
try{
|
|
459
|
+
const out=await window.cowork.askClaude(
|
|
460
|
+
"You are a patent prosecution practice manager. Using these arrays (negative daysLeft = overdue; wa2=true is an Attorney flag), write a concise Monday briefing for the managing partner. Cover: (1) projects overdue or due within ~14 days — name matter number, type, client; (2) any Attorney-flagged matters; (3) tasks needing attention by assignee; (4) one note on workload balance from assigned hours. Group by urgency, short lines, under 230 words. Do not invent data.",
|
|
461
|
+
[{projectsDueSoon:pj},{tasksDueSoon:tk},{workload:wl}]);
|
|
462
|
+
bd.textContent=(typeof out==='string'?out:(out&&out.text)||JSON.stringify(out));
|
|
463
|
+
}catch(e){ bd.textContent='Could not generate briefing: '+(e&&e.message||e); }
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
load();
|
|
467
|
+
</script>
|
|
468
|
+
</body>
|
|
469
|
+
</html>
|