<%*
// ---- Prompts ----
const rawName = await tp.system.prompt("Project name", "", true);
const name = rawName.trim();
if (!name) {
new Notice("Name is required.");
throw new Error("Empty name.");
}
const status = (await tp.system.suggester(
["Active", "On hold", "Done"],
["active", "on hold", "done"],
false,
"Status (default: active)"
)) || "active";
// ---- Helper: multi-select loop ----
async function pickMultiple(folderPaths, label) {
const allItems = app.vault.getMarkdownFiles()
.filter(f => folderPaths.some(p => f.path.startsWith(p)))
.sort((a, b) => a.basename.localeCompare(b.basename));
if (allItems.length === 0) return [];
const collected = [];
while (true) {
const remaining = allItems.filter(i => !collected.includes(i));
if (remaining.length === 0) break;
const display = ["✓ Done", ...remaining.map(i => `${i.basename} (${i.parent.name})`)];
const items = [null, ...remaining];
const placeholder = collected.length === 0
? `Link ${label} (pick Done to skip)`
: `${collected.length} linked — add another or Done`;
const choice = await tp.system.suggester(display, items, false, placeholder);
if (!choice) break;
collected.push(choice);
}
return collected;
}
const people = await pickMultiple(["People/"], "people");
const orgs = await pickMultiple([
"Organizations/Universities/",
"Organizations/Colleges/",
"Organizations/Companies/"
], "organizations");
const depts = await pickMultiple(["Organizations/Departments/"], "departments");
// ---- Build YAML lists ----
const yamlList = (items) => items.length === 0
? ""
: "\n" + items.map(i => ` - "[[${i.basename}]]"`).join("\n");
// ---- Move to Projects/ and emit frontmatter ----
await tp.file.move(`Projects/${name}`);
tR += `---
type: project
status: ${status}
people:${yamlList(people)}
organizations:${yamlList(orgs)}
departments:${yamlList(depts)}
---`;
-%>
## Overview
## Linked People
```dataview
TABLE WITHOUT ID
file.link AS "Person",
role AS "Role",
email AS "Email",
phone AS "Phone"
FROM "People"
WHERE contains(this.people, file.link)
SORT file.name ASC
```
## Linked Organizations
```dataview
TABLE WITHOUT ID
file.link AS "Organization",
default(org_type, type) AS "Kind",
email AS "Email",
phone AS "Phone"
FROM "Organizations"
WHERE contains(this.organizations, file.link) OR contains(this.departments, file.link)
SORT type ASC, file.name ASC
```
## Actions
```dataviewjs
const btn = dv.el("button", "+ New task from this note");
btn.addEventListener("click", async () => {
window._pendingTaskSource = dv.current().file.name;
const templater = app.plugins.plugins["templater-obsidian"];
if (!templater) { new Notice("Templater plugin not enabled"); return; }
const taskTpl = app.vault.getAbstractFileByPath("Templates/Task.md");
if (!taskTpl) { new Notice("Templates/Task.md not found"); return; }
await templater.templater.create_new_note_from_template(taskTpl, "", "", true);
});
```
## Open tasks
```dataviewjs
const dim = "projects";
const tasks = dv.pages('"Activities/Tasks"')
.where(p => {
if (p.status !== "open") return false;
const val = p[dim];
if (!val) return false;
const arr = Array.isArray(val) ? val : [val];
return arr.some(d => d && d.path === dv.current().file.path);
})
.sort(p => p.due, "asc");
const rows = tasks.map(p => {
const cb = document.createElement("input");
cb.type = "checkbox";
cb.addEventListener("click", async () => {
const file = app.vault.getAbstractFileByPath(p.file.path);
if (!file) return;
const content = await app.vault.read(file);
const today = new Date().toISOString().slice(0, 10);
let updated = content.replace(/^status:\s*["']?open["']?\s*$/m, "status: done");
if (/^closed:/m.test(updated)) {
updated = updated.replace(/^closed:.*$/m, `closed: ${today}`);
} else {
updated = updated.replace(/^(---\n[\s\S]*?\n)---\n/, `$1closed: ${today}\n---\n`);
}
if (updated !== content) await app.vault.modify(file, updated);
});
return [cb, p.file.link, p.due, p.date];
});
dv.table(["Done", "Task", "Due", "Created"], rows);
```
## Closed tasks
```dataviewjs
const dim = "projects";
const tasks = dv.pages('"Activities/Tasks"')
.where(p => {
if (p.status !== "done") return false;
const val = p[dim];
if (!val) return false;
const arr = Array.isArray(val) ? val : [val];
return arr.some(d => d && d.path === dv.current().file.path);
})
.sort(p => p.closed, "desc");
const rows = tasks.map(p => {
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = true;
cb.addEventListener("click", async () => {
const file = app.vault.getAbstractFileByPath(p.file.path);
if (!file) return;
const content = await app.vault.read(file);
let updated = content.replace(/^status:\s*["']?done["']?\s*$/m, "status: open");
updated = updated.replace(/^closed:.*$/m, "closed: ");
if (updated !== content) await app.vault.modify(file, updated);
});
return [cb, p.file.link, p.closed, p.due];
});
dv.table(["Reopen", "Task", "Closed", "Due"], rows);
```
## Timeline
```dataviewjs
const dim = "projects";
const all = dv.pages('"Activities"')
.where(p => {
const v = p[dim];
if (!v) return false;
const arr = Array.isArray(v) ? v : [v];
return arr.some(l => l && l.path === dv.current().file.path);
})
.sort(p => p.date, "desc");
// ===== Controls =====
const controls = dv.el("div", "", {
attr: { style: "display: flex; gap: 1em; align-items: center; margin-bottom: 0.75em; flex-wrap: wrap;" }
});
const typeWrap = controls.createEl("label", {
attr: { style: "display: inline-flex; align-items: center; gap: 0.4em;" }
});
typeWrap.createSpan({ text: "Type:" });
const typeSelect = typeWrap.createEl("select");
const typeOptions = ["All", ...[...new Set(all.map(p => p.type).filter(Boolean))].sort()];
for (const t of typeOptions) typeSelect.createEl("option", { text: t, value: t });
const rangeWrap = controls.createEl("label", {
attr: { style: "display: inline-flex; align-items: center; gap: 0.4em;" }
});
rangeWrap.createSpan({ text: "Range:" });
const rangeSelect = rangeWrap.createEl("select");
const ranges = [
{ label: "All time", value: "all" },
{ label: "Last 7 days", value: "7" },
{ label: "Last 30 days", value: "30" },
{ label: "Last 90 days", value: "90" },
{ label: "Last 365 days", value: "365" },
];
for (const r of ranges) rangeSelect.createEl("option", { text: r.label, value: r.value });
const tableContainer = dv.el("div", "");
// ===== Helpers =====
const dateISO = (p) => {
const d = p.date;
if (!d) return null;
if (d.toISODate) return d.toISODate();
if (typeof d === "string") return d.slice(0, 10);
return null;
};
const localISO = (d) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const makeWikiLink = (file, label) => {
const a = document.createElement("a");
a.className = "internal-link";
a.setAttribute("data-href", file.path);
a.setAttribute("href", file.path);
a.textContent = label || file.name;
a.addEventListener("click", (e) => {
e.preventDefault();
app.workspace.openLinkText(file.path, "", e.ctrlKey || e.metaKey);
});
return a;
};
// ===== Render =====
const render = () => {
tableContainer.empty();
const typeFilter = typeSelect.value;
const rangeFilter = rangeSelect.value;
let items = [...all];
if (typeFilter !== "All") {
items = items.filter(p => p.type === typeFilter);
}
if (rangeFilter !== "all") {
const days = parseInt(rangeFilter, 10);
const cutoff = new Date();
cutoff.setHours(0, 0, 0, 0);
cutoff.setDate(cutoff.getDate() - days);
const cutoffStr = localISO(cutoff);
items = items.filter(p => {
const d = dateISO(p);
return d && d >= cutoffStr;
});
}
if (items.length === 0) {
tableContainer.createEl("p", {
text: "No activities match the current filters.",
attr: { style: "color: var(--text-muted); font-style: italic; margin-top: 0.5em;" }
});
return;
}
const table = tableContainer.createEl("table", { cls: "dataview" });
const thead = table.createEl("thead");
const hdr = thead.createEl("tr");
for (const h of ["Note", "Type", "Date", "Subject"]) hdr.createEl("th", { text: h });
const tbody = table.createEl("tbody");
for (const item of items) {
const tr = tbody.createEl("tr");
const c1 = tr.createEl("td");
c1.appendChild(makeWikiLink(item.file));
tr.createEl("td", { text: item.type || "" });
tr.createEl("td", { text: String(item.date || "") });
const subject = item.subject || item.topic || item.file.name;
tr.createEl("td", { text: String(subject) });
}
};
typeSelect.addEventListener("change", render);
rangeSelect.addEventListener("change", render);
render();
```