Initial commit
This commit is contained in:
106
templates/appointment_detail.html
Normal file
106
templates/appointment_detail.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/appointments">Appointments</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ appointment.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;">
|
||||
<div>
|
||||
<h1 class="detail-title">{{ appointment.title }}</h1>
|
||||
<div class="detail-meta">
|
||||
{% if appointment.all_day %}
|
||||
<span class="detail-meta-item">
|
||||
<span class="status-badge status-active">All Day</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="detail-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="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"/></svg>
|
||||
{% if appointment.all_day %}
|
||||
{{ appointment.start_at.strftime('%A, %B %-d, %Y') }}
|
||||
{% if appointment.end_at and appointment.end_at.strftime('%Y-%m-%d') != appointment.start_at.strftime('%Y-%m-%d') %}
|
||||
– {{ appointment.end_at.strftime('%A, %B %-d, %Y') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ appointment.start_at.strftime('%A, %B %-d, %Y at %-I:%M %p') }}
|
||||
{% if appointment.end_at %}
|
||||
– {{ appointment.end_at.strftime('%-I:%M %p') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if appointment.location %}
|
||||
<span class="detail-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
{{ appointment.location }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if appointment.recurrence %}
|
||||
<span class="detail-meta-item">
|
||||
<span class="row-tag">{{ appointment.recurrence }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; flex-shrink: 0;">
|
||||
<a href="/appointments/{{ appointment.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form method="POST" action="/appointments/{{ appointment.id }}/delete" data-confirm="Delete this appointment?">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if appointment.description %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-title mb-2">Description</div>
|
||||
<div class="detail-body">{{ appointment.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if appointment.tags %}
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||
{% for tag in appointment.tags %}
|
||||
<span class="row-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Attendees -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Attendees ({{ contacts | length }})</span>
|
||||
</div>
|
||||
{% if contacts %}
|
||||
{% for c in contacts %}
|
||||
<div class="list-row">
|
||||
<div class="row-title">
|
||||
<a href="/contacts/{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</a>
|
||||
</div>
|
||||
{% if c.company %}
|
||||
<span class="row-meta">{{ c.company }}</span>
|
||||
{% endif %}
|
||||
{% if c.email %}
|
||||
<span class="row-meta">{{ c.email }}</span>
|
||||
{% endif %}
|
||||
{% if c.role %}
|
||||
<span class="row-tag">{{ c.role }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state" style="padding: 24px;">
|
||||
<div class="text-muted text-sm">No attendees added</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="text-muted text-xs mt-3">
|
||||
Created {{ appointment.created_at.strftime('%B %-d, %Y') }}
|
||||
{% if appointment.updated_at and appointment.updated_at != appointment.created_at %}
|
||||
· Updated {{ appointment.updated_at.strftime('%B %-d, %Y') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
130
templates/appointment_form.html
Normal file
130
templates/appointment_form.html
Normal file
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/appointments">Appointments</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ "Edit" if appointment else "New Appointment" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ "Edit Appointment" if appointment else "New Appointment" }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 720px;">
|
||||
<form method="POST" action="{{ '/appointments/' ~ appointment.id ~ '/edit' if appointment else '/appointments/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" class="form-input" required value="{{ appointment.title if appointment else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Start Date *</label>
|
||||
<input type="date" name="start_date" class="form-input" required
|
||||
value="{{ appointment.start_at.strftime('%Y-%m-%d') if appointment and appointment.start_at else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="start-time-group">
|
||||
<label class="form-label">Start Time</label>
|
||||
<input type="time" name="start_time" class="form-input" id="start-time-input"
|
||||
value="{{ appointment.start_at.strftime('%H:%M') if appointment and appointment.start_at and not appointment.all_day else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">End Date</label>
|
||||
<input type="date" name="end_date" class="form-input"
|
||||
value="{{ appointment.end_at.strftime('%Y-%m-%d') if appointment and appointment.end_at else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="end-time-group">
|
||||
<label class="form-label">End Time</label>
|
||||
<input type="time" name="end_time" class="form-input" id="end-time-input"
|
||||
value="{{ appointment.end_at.strftime('%H:%M') if appointment and appointment.end_at and not appointment.all_day else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" name="all_day" id="all-day-check"
|
||||
{{ 'checked' if appointment and appointment.all_day else '' }}
|
||||
onchange="toggleAllDay(this.checked)"
|
||||
style="width: 16px; height: 16px;">
|
||||
All Day Event
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Recurrence</label>
|
||||
<select name="recurrence" class="form-select">
|
||||
<option value="">None</option>
|
||||
<option value="daily" {{ 'selected' if appointment and appointment.recurrence == 'daily' }}>Daily</option>
|
||||
<option value="weekly" {{ 'selected' if appointment and appointment.recurrence == 'weekly' }}>Weekly</option>
|
||||
<option value="monthly" {{ 'selected' if appointment and appointment.recurrence == 'monthly' }}>Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Location</label>
|
||||
<input type="text" name="location" class="form-input" placeholder="Address, room, or video link"
|
||||
value="{{ appointment.location if appointment and appointment.location else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3" placeholder="Notes about this appointment...">{{ appointment.description if appointment and appointment.description else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="Comma-separated tags"
|
||||
value="{{ appointment.tags | join(', ') if appointment and appointment.tags else '' }}">
|
||||
</div>
|
||||
|
||||
{% if contacts %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Attendees</label>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; background: var(--surface2); border-radius: var(--radius); border: 1px solid var(--border); max-height: 200px; overflow-y: auto;">
|
||||
{% for c in contacts %}
|
||||
<label style="display: flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.85rem; cursor: pointer; white-space: nowrap;">
|
||||
<input type="checkbox" name="contact_ids" value="{{ c.id }}"
|
||||
{{ 'checked' if c.id|string in selected_contacts else '' }}
|
||||
style="width: 14px; height: 14px;">
|
||||
{{ c.first_name }} {{ c.last_name or '' }}
|
||||
{% if c.company %}<span style="color: var(--muted); font-size: 0.78rem;">({{ c.company }})</span>{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ "Update Appointment" if appointment else "Create Appointment" }}</button>
|
||||
<a href="{{ '/appointments/' ~ appointment.id if appointment else '/appointments' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleAllDay(checked) {
|
||||
const startTime = document.getElementById('start-time-input');
|
||||
const endTime = document.getElementById('end-time-input');
|
||||
if (checked) {
|
||||
startTime.disabled = true;
|
||||
startTime.style.opacity = '0.4';
|
||||
endTime.disabled = true;
|
||||
endTime.style.opacity = '0.4';
|
||||
} else {
|
||||
startTime.disabled = false;
|
||||
startTime.style.opacity = '1';
|
||||
endTime.disabled = false;
|
||||
endTime.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
// Init on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const cb = document.getElementById('all-day-check');
|
||||
if (cb && cb.checked) toggleAllDay(true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
72
templates/appointments.html
Normal file
72
templates/appointments.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Appointments <span class="page-count">({{ count }})</span></h1>
|
||||
</div>
|
||||
<a href="/appointments/new" class="btn btn-primary">+ New Appointment</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar">
|
||||
<a href="/appointments?timeframe=upcoming" class="btn {{ 'btn-primary' if timeframe == 'upcoming' else 'btn-secondary' }} btn-sm">Upcoming</a>
|
||||
<a href="/appointments?timeframe=past" class="btn {{ 'btn-primary' if timeframe == 'past' else 'btn-secondary' }} btn-sm">Past</a>
|
||||
<a href="/appointments?timeframe=all" class="btn {{ 'btn-primary' if timeframe == 'all' else 'btn-secondary' }} btn-sm">All</a>
|
||||
</div>
|
||||
|
||||
{% if appointments %}
|
||||
<div class="card">
|
||||
{% set current_date = namespace(value='') %}
|
||||
{% for appt in appointments %}
|
||||
{% set appt_date = appt.start_at.strftime('%A, %B %-d, %Y') if appt.start_at else 'No Date' %}
|
||||
{% if appt_date != current_date.value %}
|
||||
{% if not loop.first %}</div>{% endif %}
|
||||
<div class="date-group-label" style="padding: 12px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
{{ appt_date }}
|
||||
</div>
|
||||
<div>
|
||||
{% set current_date.value = appt_date %}
|
||||
{% endif %}
|
||||
|
||||
<div class="list-row">
|
||||
<div style="flex-shrink: 0; min-width: 60px;">
|
||||
{% if appt.all_day %}
|
||||
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>
|
||||
{% elif appt.start_at %}
|
||||
<span style="font-size: 0.85rem; font-weight: 600; color: var(--text);">{{ appt.start_at.strftime('%-I:%M %p') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row-title">
|
||||
<a href="/appointments/{{ appt.id }}">{{ appt.title }}</a>
|
||||
</div>
|
||||
{% if appt.location %}
|
||||
<span class="row-meta">{{ appt.location }}</span>
|
||||
{% endif %}
|
||||
{% if appt.recurrence %}
|
||||
<span class="row-tag">{{ appt.recurrence }}</span>
|
||||
{% endif %}
|
||||
{% if appt.contact_count and appt.contact_count > 0 %}
|
||||
<span class="row-meta">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: -2px;"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
{{ appt.contact_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/appointments/{{ appt.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form method="POST" action="/appointments/{{ appt.id }}/delete" data-confirm="Delete this appointment?">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red);">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📅</div>
|
||||
<div class="empty-state-text">No appointments {{ 'upcoming' if timeframe == 'upcoming' else 'found' }}</div>
|
||||
<a href="/appointments/new" class="btn btn-primary">Schedule an Appointment</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
22
templates/area_form.html
Normal file
22
templates/area_form.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/areas">Areas</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Area' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Area' if item else 'New Area' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/areas/' ~ item.id ~ '/edit' if item else '/areas/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label class="form-label">Name *</label><input type="text" name="name" class="form-input" required value="{{ item.name if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}
|
||||
</select></div>
|
||||
<div class="form-group"><label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="active" {{ 'selected' if not item or item.status == 'active' }}>Active</option>
|
||||
<option value="inactive" {{ 'selected' if item and item.status == 'inactive' }}>Inactive</option>
|
||||
</select></div>
|
||||
<div class="form-group full-width"><label class="form-label">Description</label><textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea></div>
|
||||
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="/areas" class="btn btn-secondary">Cancel</a></div>
|
||||
</div>
|
||||
</form></div>
|
||||
{% endblock %}
|
||||
25
templates/areas.html
Normal file
25
templates/areas.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Areas<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/areas/create" class="btn btn-primary">+ New Area</a>
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-domain-tag" style="background:{{ item.domain_color or '#4F6EF7' }}22;color:{{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
|
||||
<span class="row-title">{{ item.name }}</span>
|
||||
{% if item.description %}<span class="row-meta">{{ item.description[:60] }}</span>{% endif %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/areas/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/areas/{{ item.id }}/delete" method="post" data-confirm="Delete this area?" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No areas yet</div><a href="/areas/create" class="btn btn-primary">Create Area</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
241
templates/base.html
Normal file
241
templates/base.html
Normal file
@@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ page_title or "Life OS" }} - Life OS</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<style>
|
||||
/* Critical mob-nav positioning - inline to guarantee it loads */
|
||||
.mob-nav{display:none}
|
||||
@media(max-width:768px){
|
||||
.mob-nav{display:flex!important;position:fixed!important;bottom:0!important;left:0!important;right:0!important;height:56px!important;background:var(--surface)!important;border-top:1px solid var(--border)!important;z-index:9999!important;flex-direction:row!important;flex-wrap:nowrap!important;align-items:center!important;justify-content:space-around!important;padding:0!important;margin:0!important;overflow:hidden!important}
|
||||
.mob-overlay{display:none!important;position:fixed!important;top:0!important;left:0!important;right:0!important;bottom:0!important;background:rgba(0,0,0,.5)!important;z-index:10000!important}
|
||||
.mob-overlay.open{display:block!important}
|
||||
.mob-more{display:none!important;position:fixed!important;bottom:56px!important;left:0!important;right:0!important;background:var(--surface)!important;border-top:1px solid var(--border)!important;border-radius:12px 12px 0 0!important;z-index:10001!important;padding:16px!important;grid-template-columns:repeat(3,1fr)!important;gap:8px!important}
|
||||
.mob-more.open{display:grid!important}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/" class="sidebar-logo">Life<span>OS</span></a>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<a href="/" class="nav-item {{ 'active' if active_nav == 'dashboard' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/focus" class="nav-item {{ 'active' if active_nav == 'focus' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
Focus
|
||||
{% if sidebar.focus_count %}<span class="badge">{{ sidebar.focus_count }}</span>{% endif %}
|
||||
</a>
|
||||
<a href="/tasks" class="nav-item {{ 'active' if active_nav == 'tasks' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
||||
All Tasks
|
||||
</a>
|
||||
<a href="/projects" class="nav-item {{ 'active' if active_nav == 'projects' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
Projects
|
||||
</a>
|
||||
<a href="/notes" class="nav-item {{ 'active' if active_nav == 'notes' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
|
||||
Notes
|
||||
</a>
|
||||
<a href="/contacts" class="nav-item {{ 'active' if active_nav == 'contacts' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
Contacts
|
||||
</a>
|
||||
<a href="/links" class="nav-item {{ 'active' if active_nav == 'links' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
Links
|
||||
</a>
|
||||
<a href="/lists" class="nav-item {{ 'active' if active_nav == 'lists' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
Lists
|
||||
</a>
|
||||
<a href="/processes" class="nav-item {{ 'active' if active_nav == 'processes' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
Processes
|
||||
</a>
|
||||
<a href="/meetings" class="nav-item {{ 'active' if active_nav == 'meetings' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Meetings
|
||||
</a>
|
||||
<a href="/appointments" class="nav-item {{ 'active' if active_nav == 'appointments' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="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"/></svg>
|
||||
Appointments
|
||||
</a>
|
||||
<a href="/calendar" class="nav-item {{ 'active' if active_nav == 'calendar' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="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"/><rect x="7" y="14" width="3" height="3" rx="0.5"/><rect x="14" y="14" width="3" height="3" rx="0.5"/></svg>
|
||||
Calendar
|
||||
</a>
|
||||
<a href="/eisenhower" class="nav-item {{ 'active' if active_nav == 'eisenhower' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg>
|
||||
Eisenhower
|
||||
</a>
|
||||
<a href="/decisions" class="nav-item {{ 'active' if active_nav == 'decisions' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9 12l2 2 4-4"/></svg>
|
||||
Decisions
|
||||
</a>
|
||||
<a href="/files" class="nav-item {{ 'active' if active_nav == 'files' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
Files
|
||||
</a>
|
||||
<a href="/weblinks" class="nav-item {{ 'active' if active_nav == 'bookmarks' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Bookmarks
|
||||
</a>
|
||||
<a href="/time" class="nav-item {{ 'active' if active_nav == 'time' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
Time Log
|
||||
</a>
|
||||
<a href="/time-budgets" class="nav-item {{ 'active' if active_nav == 'time_budgets' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/><path d="M16 3l2 2-2 2"/></svg>
|
||||
Time Budgets
|
||||
</a>
|
||||
<a href="/capture" class="nav-item {{ 'active' if active_nav == 'capture' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
|
||||
Capture
|
||||
{% if sidebar.capture_count %}<span class="badge">{{ sidebar.capture_count }}</span>{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Domain hierarchy -->
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-label">Domains</div>
|
||||
{% for domain in sidebar.domain_tree %}
|
||||
<div class="domain-group">
|
||||
<div class="domain-header" data-domain-id="{{ domain.id }}">
|
||||
<span class="domain-dot" style="background: {{ domain.color or '#4F6EF7' }}"></span>
|
||||
{{ domain.name }}
|
||||
</div>
|
||||
<div class="domain-children" data-domain-id="{{ domain.id }}">
|
||||
{% for area in domain.areas %}
|
||||
<div class="area-label">{{ area.name }}</div>
|
||||
{% for p in area.projects %}
|
||||
<a href="/projects/{{ p.id }}" class="project-link">{{ p.name }}</a>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for p in domain.standalone_projects %}
|
||||
<a href="/projects/{{ p.id }}" class="project-link">{{ p.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="nav-section" style="margin-top: auto; padding-bottom: 12px;">
|
||||
<a href="/history" class="nav-item {{ 'active' if active_nav == 'history' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
History
|
||||
</a>
|
||||
<a href="/admin/trash" class="nav-item {{ 'active' if active_nav == 'trash' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
Trash
|
||||
</a>
|
||||
<a href="/domains" class="nav-item {{ 'active' if active_nav == 'domains' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m-7-7h6m6 0h6"/></svg>
|
||||
Manage Domains
|
||||
</a>
|
||||
<a href="/areas" class="nav-item {{ 'active' if active_nav == 'areas' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
|
||||
Manage Areas
|
||||
</a>
|
||||
<div class="nav-item" onclick="toggleTheme()" style="cursor: pointer;">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
Toggle Theme
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="main-content">
|
||||
<header class="topbar">
|
||||
{% if request.state.environment == 'development' %}
|
||||
<span class="topbar-env">DEV</span>
|
||||
{% endif %}
|
||||
<div class="topbar-spacer"></div>
|
||||
<!-- Timer Pill (populated by JS) -->
|
||||
<div id="timer-pill" class="timer-pill hidden">
|
||||
<div class="timer-pill-dot"></div>
|
||||
<a id="timer-pill-task" class="timer-pill-task" href="#"></a>
|
||||
<span id="timer-pill-elapsed" class="timer-pill-elapsed">0:00</span>
|
||||
<form method="POST" action="/time/stop" style="display:inline;">
|
||||
<button type="submit" class="timer-pill-stop" title="Stop timer">■</button>
|
||||
</form>
|
||||
</div>
|
||||
<form class="quick-capture-form" method="POST" action="/capture/add">
|
||||
<input type="hidden" name="redirect_to" value="{{ request.url.path }}{% if request.url.query %}?{{ request.url.query }}{% endif %}">
|
||||
<span class="quick-capture-icon">+</span>
|
||||
<input type="text" name="raw_text" class="quick-capture-input" placeholder="Quick capture..." autocomplete="off" required>
|
||||
<button type="submit" class="quick-capture-submit" title="Capture">+</button>
|
||||
</form>
|
||||
<button class="search-trigger" onclick="openSearch()" title="Search (Cmd/K)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<span>Search...</span>
|
||||
<kbd>⌘K</kbd>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal" class="search-modal hidden">
|
||||
<div class="search-modal-backdrop" onclick="closeSearch()"></div>
|
||||
<div class="search-modal-content">
|
||||
<div class="search-modal-input-wrap">
|
||||
<svg class="search-modal-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" class="search-modal-input" placeholder="Search tasks, projects, notes..." autocomplete="off">
|
||||
<kbd class="search-modal-esc" onclick="closeSearch()">Esc</kbd>
|
||||
</div>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
|
||||
<div class="mob-nav" id="mobNav" style="position:fixed;bottom:0;left:0;right:0;z-index:9999">
|
||||
<a href="/" class="mob-nav__item {% if request.url.path == '/' %}mob-nav__item--active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="/focus" class="mob-nav__item {% if '/focus' in request.url.path %}mob-nav__item--active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
<span>Focus</span>
|
||||
</a>
|
||||
<a href="/tasks" class="mob-nav__item {% if '/tasks' in request.url.path %}mob-nav__item--active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
|
||||
<span>Tasks</span>
|
||||
</a>
|
||||
<a href="/capture" class="mob-nav__item {% if '/capture' in request.url.path %}mob-nav__item--active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
<span>Capture</span>
|
||||
</a>
|
||||
<button class="mob-nav__item" id="mobMoreBtn" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
<span>More</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mob-overlay" id="mobOverlay"></div>
|
||||
<div class="mob-more" id="mobMore">
|
||||
<a href="/calendar" class="mob-more__item"><span>Calendar</span></a>
|
||||
<a href="/notes" class="mob-more__item"><span>Notes</span></a>
|
||||
<a href="/meetings" class="mob-more__item"><span>Meetings</span></a>
|
||||
<a href="/decisions" class="mob-more__item"><span>Decisions</span></a>
|
||||
<a href="/contacts" class="mob-more__item"><span>Contacts</span></a>
|
||||
<a href="/processes" class="mob-more__item"><span>Processes</span></a>
|
||||
<a href="/weblinks" class="mob-more__item"><span>Bookmarks</span></a>
|
||||
<a href="/admin" class="mob-more__item"><span>Admin</span></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
58
templates/calendar.html
Normal file
58
templates/calendar.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Calendar</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Month Navigation -->
|
||||
<div class="cal-nav">
|
||||
<a href="/calendar?year={{ prev_year }}&month={{ prev_month }}" class="btn btn-secondary btn-sm">← Prev</a>
|
||||
<span class="cal-month-label">{{ month_name }} {{ year }}</span>
|
||||
<a href="/calendar?year={{ next_year }}&month={{ next_month }}" class="btn btn-secondary btn-sm">Next →</a>
|
||||
{% if year != today.year or month != today.month %}
|
||||
<a href="/calendar" class="btn btn-ghost btn-sm">Today</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="cal-legend">
|
||||
<span class="cal-legend-item"><span class="cal-dot cal-dot-appointment"></span> Appointment</span>
|
||||
<span class="cal-legend-item"><span class="cal-dot cal-dot-meeting"></span> Meeting</span>
|
||||
<span class="cal-legend-item"><span class="cal-dot cal-dot-task"></span> Task</span>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Grid -->
|
||||
<div class="cal-grid">
|
||||
<div class="cal-header-row">
|
||||
<div class="cal-header-cell">Sun</div>
|
||||
<div class="cal-header-cell">Mon</div>
|
||||
<div class="cal-header-cell">Tue</div>
|
||||
<div class="cal-header-cell">Wed</div>
|
||||
<div class="cal-header-cell">Thu</div>
|
||||
<div class="cal-header-cell">Fri</div>
|
||||
<div class="cal-header-cell">Sat</div>
|
||||
</div>
|
||||
{% for week in weeks %}
|
||||
<div class="cal-week-row">
|
||||
{% for day in week %}
|
||||
<div class="cal-day-cell {{ 'cal-day-empty' if day == 0 }} {{ 'cal-day-today' if day > 0 and today.year == year and today.month == month and today.day == day }}">
|
||||
{% if day > 0 %}
|
||||
<div class="cal-day-num">{{ day }}</div>
|
||||
<div class="cal-events">
|
||||
{% for event in days_map.get(day, []) %}
|
||||
<a href="{{ event.url }}" class="cal-event cal-event-{{ event.type }}" title="{{ event.title }}">
|
||||
{% if event.time %}<span class="cal-event-time">{{ event.time }}</span>{% endif %}
|
||||
<span class="cal-event-title">{{ event.title }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
110
templates/capture.html
Normal file
110
templates/capture.html
Normal file
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Capture<span class="page-count">{{ items|length }}</span></h1>
|
||||
</div>
|
||||
|
||||
<!-- Quick capture input -->
|
||||
<div class="card mb-4 capture-form-card">
|
||||
<form action="/capture/add" method="post">
|
||||
<label class="form-label mb-2">Quick Capture (one item per line)</label>
|
||||
<textarea name="raw_text" class="form-textarea" rows="3" placeholder="Type or paste to capture..." required></textarea>
|
||||
<details class="mt-2">
|
||||
<summary class="text-sm text-muted" style="cursor:pointer">Context (optional)</summary>
|
||||
<div class="form-grid mt-2" style="grid-template-columns:1fr 1fr">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Area</label>
|
||||
<select name="area_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
{% for a in d.areas %}
|
||||
<option value="{{ a.id }}">{{ d.name }} / {{ a.name }}</option>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="mt-2"><button type="submit" class="btn btn-primary btn-capture">Capture</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter tabs -->
|
||||
<div class="tab-strip mb-3">
|
||||
<a href="/capture?show=inbox" class="tab-item {{ 'active' if show == 'inbox' or show not in ('processed', 'all') }}">Inbox</a>
|
||||
<a href="/capture?show=processed" class="tab-item {{ 'active' if show == 'processed' }}">Processed</a>
|
||||
<a href="/capture?show=all" class="tab-item {{ 'active' if show == 'all' }}">All</a>
|
||||
</div>
|
||||
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
|
||||
{# Batch header #}
|
||||
{% if item._batch_first and item.import_batch_id %}
|
||||
<div class="flex items-center justify-between mb-2 mt-3" style="padding:6px 12px;background:var(--surface2);border-radius:var(--radius-sm)">
|
||||
<span class="text-xs text-muted">Batch · {{ batches[item.import_batch_id|string] }} items</span>
|
||||
<form action="/capture/batch/{{ item.import_batch_id }}/undo" method="post" style="display:inline" data-confirm="Delete all items in this batch?">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Undo batch</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="capture-item {{ 'completed' if item.processed }}">
|
||||
<div class="capture-text {{ 'text-muted' if item.processed }}" style="{{ 'text-decoration:line-through' if item.processed }}">{{ item.raw_text }}</div>
|
||||
|
||||
{% if item.processed %}
|
||||
<div class="capture-actions">
|
||||
{% if item.converted_to_type and item.converted_to_type != 'dismissed' %}
|
||||
<span class="row-tag">{{ item.converted_to_type|replace('_', ' ') }}</span>
|
||||
{% if item.converted_to_id %}
|
||||
{% if item.converted_to_type == 'list_item' and item.list_id %}
|
||||
<a href="/lists/{{ item.list_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% elif item.converted_to_type == 'link' %}
|
||||
<a href="/links" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% elif item.converted_to_type in ('task', 'note', 'project', 'contact', 'decision') %}
|
||||
<a href="/{{ item.converted_to_type }}s/{{ item.converted_to_id }}" class="btn btn-ghost btn-xs">View →</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif item.converted_to_type == 'dismissed' %}
|
||||
<span class="row-tag" style="color:var(--muted)">dismissed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="capture-actions">
|
||||
<select onchange="if(this.value) window.location.href=this.value" class="filter-select" style="font-size:0.75rem;padding:3px 6px">
|
||||
<option value="">Convert...</option>
|
||||
{% for key, label in convert_types.items() %}
|
||||
<option value="/capture/{{ item.id }}/convert/{{ key }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<form action="/capture/{{ item.id }}/dismiss" method="post" style="display:inline"><button class="btn btn-ghost btn-xs">Dismiss</button></form>
|
||||
<form action="/capture/{{ item.id }}/delete" method="post" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">×</button></form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📥</div>
|
||||
<div class="empty-state-text">
|
||||
{% if show == 'processed' %}No processed items{% elif show == 'all' %}No capture items{% else %}Inbox is empty{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
153
templates/capture_convert.html
Normal file
153
templates/capture_convert.html
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/capture">Capture</a> <span class="sep">/</span> Convert to {{ type_label }}
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Convert to {{ type_label }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- Original text -->
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Original Text</label>
|
||||
<div style="padding:10px 12px;background:var(--surface2);border-radius:var(--radius);font-size:0.92rem;color:var(--text-secondary)">{{ item.raw_text }}</div>
|
||||
</div>
|
||||
|
||||
<form action="/capture/{{ item.id }}/to-{{ convert_type }}" method="post">
|
||||
<div class="form-grid">
|
||||
|
||||
{% if convert_type == 'task' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="1">1 - Critical</option>
|
||||
<option value="2">2 - High</option>
|
||||
<option value="3" selected>3 - Normal</option>
|
||||
<option value="4">4 - Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'note' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text[:100] }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain</label>
|
||||
<select name="domain_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<optgroup label="{{ d.name }}">
|
||||
{% for a in d.areas %}{% for p in a.projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}{% endfor %}
|
||||
{% for p in d.standalone_projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if item.project_id and p.id|string == item.project_id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'project' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Project Name</label>
|
||||
<input type="text" name="name" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in sidebar.domain_tree %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'list_item' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Content</label>
|
||||
<input type="text" name="content" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Add to List *</label>
|
||||
<select name="list_id" class="form-select" required>
|
||||
{% for lst in all_lists %}
|
||||
<option value="{{ lst.id }}" {{ 'selected' if item.list_id and lst.id|string == item.list_id|string }}>{{ lst.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'contact' %}
|
||||
<div class="form-group">
|
||||
<label class="form-label">First Name *</label>
|
||||
<input type="text" name="first_name" class="form-input" value="{{ first_name }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Last Name</label>
|
||||
<input type="text" name="last_name" class="form-input" value="{{ last_name }}">
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'decision' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Decision Title</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ item.raw_text }}" required>
|
||||
</div>
|
||||
|
||||
{% elif convert_type == 'link' %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" name="label" class="form-input" value="{{ prefill_label }}" required>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="url" name="url" class="form-input" value="{{ prefill_url }}" placeholder="https://" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Convert to {{ type_label }}</button>
|
||||
<a href="/capture" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
templates/contact_detail.html
Normal file
25
templates/contact_detail.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/contacts">Contacts</a><span class="sep">/</span><span>{{ item.first_name }} {{ item.last_name or '' }}</span></div>
|
||||
<div class="detail-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="detail-title">{{ item.first_name }} {{ item.last_name or '' }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/contacts/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/contacts/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-danger btn-sm">Delete</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta mt-2">
|
||||
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
|
||||
{% if item.role %}<span class="detail-meta-item">{{ item.role }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="form-grid" style="gap:12px">
|
||||
{% if item.email %}<div class="form-group"><div class="form-label">Email</div><a href="mailto:{{ item.email }}">{{ item.email }}</a></div>{% endif %}
|
||||
{% if item.phone %}<div class="form-group"><div class="form-label">Phone</div><a href="tel:{{ item.phone }}">{{ item.phone }}</a></div>{% endif %}
|
||||
{% if item.notes %}<div class="form-group full-width"><div class="form-label">Notes</div><div class="detail-body" style="white-space:pre-wrap">{{ item.notes }}</div></div>{% endif %}
|
||||
{% if item.tags %}<div class="form-group full-width"><div class="form-label">Tags</div><div class="flex gap-2">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
templates/contact_form.html
Normal file
19
templates/contact_form.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/contacts">Contacts</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Contact' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Contact' if item else 'New Contact' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/contacts/' ~ item.id ~ '/edit' if item else '/contacts/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label class="form-label">First Name *</label><input type="text" name="first_name" class="form-input" required value="{{ item.first_name if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Last Name</label><input type="text" name="last_name" class="form-input" value="{{ item.last_name if item and item.last_name else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Company</label><input type="text" name="company" class="form-input" value="{{ item.company if item and item.company else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Role</label><input type="text" name="role" class="form-input" value="{{ item.role if item and item.role else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Email</label><input type="email" name="email" class="form-input" value="{{ item.email if item and item.email else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Phone</label><input type="tel" name="phone" class="form-input" value="{{ item.phone if item and item.phone else '' }}"></div>
|
||||
<div class="form-group full-width"><label class="form-label">Notes</label><textarea name="notes" class="form-textarea" rows="4">{{ item.notes if item and item.notes else '' }}</textarea></div>
|
||||
<div class="form-group full-width"><label class="form-label">Tags</label><input type="text" name="tags" class="form-input" value="{{ item.tags|join(', ') if item and item.tags else '' }}"></div>
|
||||
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="/contacts" class="btn btn-secondary">Cancel</a></div>
|
||||
</div>
|
||||
</form></div>
|
||||
{% endblock %}
|
||||
25
templates/contacts.html
Normal file
25
templates/contacts.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Contacts<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/contacts/create" class="btn btn-primary">+ New Contact</a>
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span>
|
||||
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
|
||||
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}
|
||||
{% if item.email %}<span class="row-meta">{{ item.email }}</span>{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/contacts/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/contacts/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-icon">👤</div><div class="empty-state-text">No contacts yet</div><a href="/contacts/create" class="btn btn-primary">Add Contact</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
140
templates/dashboard.html
Normal file
140
templates/dashboard.html
Normal file
@@ -0,0 +1,140 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="dashboard-grid mb-4">
|
||||
<a href="/tasks/?status=open" class="stat-card-link">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.open_tasks or 0 }}</div>
|
||||
<div class="stat-label">Open Tasks</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/tasks/?status=in_progress" class="stat-card-link">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.in_progress or 0 }}</div>
|
||||
<div class="stat-label">In Progress</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/tasks/?status=done" class="stat-card-link">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.done_this_week or 0 }}</div>
|
||||
<div class="stat-label">Done This Week</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/focus/" class="stat-card-link">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ focus_items|length }}</div>
|
||||
<div class="stat-label">Today's Focus</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Today's Focus -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Today's Focus</h2>
|
||||
<a href="/focus" class="btn btn-ghost btn-sm">View All</a>
|
||||
</div>
|
||||
{% if focus_items %}
|
||||
{% for item in focus_items %}
|
||||
<div class="focus-item {{ 'completed' if item.completed }}">
|
||||
<span class="priority-dot priority-{{ item.priority }}"></span>
|
||||
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
|
||||
{% if item.project_name %}
|
||||
<span class="row-meta">{{ item.project_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-text">No focus items for today</div>
|
||||
<a href="/focus" class="btn btn-primary btn-sm">Set Focus</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Overdue + Upcoming -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Upcoming</h2>
|
||||
</div>
|
||||
{% if overdue_tasks %}
|
||||
<div class="text-xs text-muted mb-2" style="font-weight:600; color: var(--red);">OVERDUE</div>
|
||||
{% for t in overdue_tasks %}
|
||||
<div class="list-row">
|
||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ t.id }}">{{ t.title }}</a></span>
|
||||
<span class="row-meta overdue">{{ t.due_date }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if upcoming_tasks %}
|
||||
<div class="text-xs text-muted mb-2 {{ 'mt-3' if overdue_tasks }}" style="font-weight:600;">NEXT 7 DAYS</div>
|
||||
{% for t in upcoming_tasks %}
|
||||
<div class="list-row">
|
||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ t.id }}">{{ t.title }}</a></span>
|
||||
<span class="row-meta">{{ t.due_date }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if not overdue_tasks and not upcoming_tasks %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-text">No upcoming deadlines</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Deadlines -->
|
||||
{% if overdue_projects or upcoming_projects %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Project Deadlines</h2>
|
||||
<a href="/projects/" class="btn btn-ghost btn-sm">All Projects</a>
|
||||
</div>
|
||||
|
||||
{% if overdue_projects %}
|
||||
<div class="text-xs text-muted mb-2" style="font-weight:600; color: var(--red);">OVERDUE</div>
|
||||
{% for p in overdue_projects %}
|
||||
<div class="list-row">
|
||||
<span class="priority-dot priority-{{ p.priority }}"></span>
|
||||
<span class="row-title"><a href="/projects/{{ p.id }}">{{ p.name }}</a></span>
|
||||
{% if p.domain_name %}
|
||||
<span class="row-meta">{{ p.domain_name }}</span>
|
||||
{% endif %}
|
||||
<div class="project-progress-mini">
|
||||
<div class="project-progress-bar" style="width: {{ ((p.done_count / p.task_count * 100) if p.task_count else 0)|int }}%"></div>
|
||||
</div>
|
||||
<span class="row-meta" style="min-width: 32px; text-align: right; font-size: 0.72rem;">{{ p.done_count }}/{{ p.task_count }}</span>
|
||||
<span class="row-meta overdue">{{ p.target_date }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if upcoming_projects %}
|
||||
<div class="text-xs text-muted mb-2 {{ 'mt-3' if overdue_projects }}" style="font-weight:600;">NEXT 30 DAYS</div>
|
||||
{% for p in upcoming_projects %}
|
||||
<div class="list-row">
|
||||
<span class="priority-dot priority-{{ p.priority }}"></span>
|
||||
<span class="row-title"><a href="/projects/{{ p.id }}">{{ p.name }}</a></span>
|
||||
{% if p.domain_name %}
|
||||
<span class="row-meta">{{ p.domain_name }}</span>
|
||||
{% endif %}
|
||||
<div class="project-progress-mini">
|
||||
<div class="project-progress-bar" style="width: {{ ((p.done_count / p.task_count * 100) if p.task_count else 0)|int }}%"></div>
|
||||
</div>
|
||||
<span class="row-meta" style="min-width: 32px; text-align: right; font-size: 0.72rem;">{{ p.done_count }}/{{ p.task_count }}</span>
|
||||
<span class="row-meta">{{ p.target_date }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
55
templates/decision_detail.html
Normal file
55
templates/decision_detail.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/decisions">Decisions</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.title }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/decisions/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/decisions/{{ item.id }}/delete" method="post" data-confirm="Delete this decision?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }} impact</span>
|
||||
{% if item.decided_at %}<span class="detail-meta-item">Decided: {{ item.decided_at }}</span>{% endif %}
|
||||
{% if meeting %}<span class="detail-meta-item">Meeting: <a href="/meetings/{{ meeting.id }}">{{ meeting.title }}</a></span>{% endif %}
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if superseded_by %}
|
||||
<div class="card mt-3" style="border-left: 3px solid var(--amber);">
|
||||
<div style="padding: 12px 16px;">
|
||||
<strong style="color: var(--amber);">Superseded by:</strong>
|
||||
<a href="/decisions/{{ superseded_by.id }}">{{ superseded_by.title }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.rationale %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Rationale</h3></div>
|
||||
<div class="detail-body" style="padding: 12px 16px;">{{ item.rationale }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if supersedes %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Supersedes</h3></div>
|
||||
{% for dec in supersedes %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ dec.id }}">{{ dec.title }}</a></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
87
templates/decision_form.html
Normal file
87
templates/decision_form.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/decisions/' ~ item.id ~ '/edit' if item else '/decisions/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" class="form-input" required
|
||||
value="{{ item.title if item else '' }}" placeholder="Summary of the decision...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="proposed" {{ 'selected' if (item and item.status == 'proposed') or not item }}>Proposed</option>
|
||||
<option value="accepted" {{ 'selected' if item and item.status == 'accepted' }}>Accepted</option>
|
||||
<option value="rejected" {{ 'selected' if item and item.status == 'rejected' }}>Rejected</option>
|
||||
<option value="superseded" {{ 'selected' if item and item.status == 'superseded' }}>Superseded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Impact</label>
|
||||
<select name="impact" class="form-select">
|
||||
<option value="low" {{ 'selected' if item and item.impact == 'low' }}>Low</option>
|
||||
<option value="medium" {{ 'selected' if (item and item.impact == 'medium') or not item }}>Medium</option>
|
||||
<option value="high" {{ 'selected' if item and item.impact == 'high' }}>High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Decision Date</label>
|
||||
<input type="date" name="decided_at" class="form-input"
|
||||
value="{{ item.decided_at if item and item.decided_at else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Meeting</label>
|
||||
<select name="meeting_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for m in meetings %}
|
||||
<option value="{{ m.id }}"
|
||||
{{ 'selected' if (item and item.meeting_id and item.meeting_id|string == m.id|string) or (not item and prefill_meeting_id == m.id|string) }}>
|
||||
{{ m.title }} ({{ m.meeting_date }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% if item and other_decisions is defined %}
|
||||
<div class="form-group">
|
||||
<label class="form-label">Superseded By</label>
|
||||
<select name="superseded_by_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for d in other_decisions %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if item.superseded_by_id and item.superseded_by_id|string == d.id|string }}>
|
||||
{{ d.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Rationale</label>
|
||||
<textarea name="rationale" class="form-textarea" rows="6" placeholder="Why was this decided? What alternatives were considered?">{{ item.rationale if item and item.rationale else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Record Decision' }}</button>
|
||||
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=decisions' if prefill_task_id is defined and prefill_task_id else ('/decisions/' ~ item.id if item else '/decisions') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
53
templates/decisions.html
Normal file
53
templates/decisions.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Decisions<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/decisions/create" class="btn btn-primary">+ New Decision</a>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/decisions">
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="proposed" {{ 'selected' if current_status == 'proposed' }}>Proposed</option>
|
||||
<option value="accepted" {{ 'selected' if current_status == 'accepted' }}>Accepted</option>
|
||||
<option value="rejected" {{ 'selected' if current_status == 'rejected' }}>Rejected</option>
|
||||
<option value="superseded" {{ 'selected' if current_status == 'superseded' }}>Superseded</option>
|
||||
</select>
|
||||
<select name="impact" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Impact</option>
|
||||
<option value="high" {{ 'selected' if current_impact == 'high' }}>High</option>
|
||||
<option value="medium" {{ 'selected' if current_impact == 'medium' }}>Medium</option>
|
||||
<option value="low" {{ 'selected' if current_impact == 'low' }}>Low</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span>
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>
|
||||
{% if item.decided_at %}
|
||||
<span class="row-meta">{{ item.decided_at }}</span>
|
||||
{% endif %}
|
||||
{% if item.meeting_title %}
|
||||
<span class="row-meta">{{ item.meeting_title }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/decisions/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/decisions/{{ item.id }}/delete" method="post" data-confirm="Delete this decision?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">⚖</div>
|
||||
<div class="empty-state-text">No decisions recorded yet</div>
|
||||
<a href="/decisions/create" class="btn btn-primary">Record First Decision</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
17
templates/domain_form.html
Normal file
17
templates/domain_form.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/domains">Domains</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Domain' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Domain' if item else 'New Domain' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/domains/' ~ item.id ~ '/edit' if item else '/domains/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label class="form-label">Name *</label><input type="text" name="name" class="form-input" required value="{{ item.name if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Color</label><input type="color" name="color" class="form-input" value="{{ item.color if item and item.color else '#4F6EF7' }}" style="height:42px;padding:4px"></div>
|
||||
<div class="form-group full-width"><label class="form-label">Description</label><textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea></div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button>
|
||||
<a href="/domains" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form></div>
|
||||
{% endblock %}
|
||||
26
templates/domains.html
Normal file
26
templates/domains.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Domains<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/domains/create" class="btn btn-primary">+ New Domain</a>
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="domain-dot" style="background: {{ item.color or '#4F6EF7' }}"></span>
|
||||
<span class="row-title"><a href="/tasks?domain_id={{ item.id }}">{{ item.name }}</a></span>
|
||||
{% if item.description %}<span class="row-meta">{{ item.description }}</span>{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/domains/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/domains/{{ item.id }}/delete" method="post" data-confirm="Delete this domain and all its contents?" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No domains. Create your first life domain.</div><a href="/domains/create" class="btn btn-primary">Create Domain</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
184
templates/eisenhower.html
Normal file
184
templates/eisenhower.html
Normal file
@@ -0,0 +1,184 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Eisenhower Matrix</h1>
|
||||
<span class="text-muted">{{ total }} open tasks classified by priority & urgency</span>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form class="filters-bar" method="get" action="/eisenhower" id="eis-filters">
|
||||
<select name="domain_id" class="filter-select" id="eis-domain" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="project_id" class="filter-select" id="eis-project" onchange="this.form.submit()">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="open" {{ 'selected' if current_status == 'open' }}>Open</option>
|
||||
<option value="in_progress" {{ 'selected' if current_status == 'in_progress' }}>In Progress</option>
|
||||
<option value="blocked" {{ 'selected' if current_status == 'blocked' }}>Blocked</option>
|
||||
</select>
|
||||
<select name="context" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Contexts</option>
|
||||
{% for ct in context_types %}
|
||||
<option value="{{ ct.value }}" {{ 'selected' if current_context == ct.value }}>{{ ct.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<div class="eisenhower-grid">
|
||||
<!-- Axis labels -->
|
||||
<div class="eisenhower-y-label">
|
||||
<span>Important</span>
|
||||
</div>
|
||||
|
||||
<!-- Q1: Urgent + Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q1">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Do First</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Urgent & Important</span>
|
||||
<span class="badge badge-red">{{ counts.do_first }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.do_first %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
|
||||
{{ task.due_date.strftime('%b %-d') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Q2: Not Urgent + Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q2">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Schedule</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Not Urgent & Important</span>
|
||||
<span class="badge badge-accent">{{ counts.schedule }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.schedule %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second row y-label -->
|
||||
<div class="eisenhower-y-label">
|
||||
<span>Not Important</span>
|
||||
</div>
|
||||
|
||||
<!-- Q3: Urgent + Not Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q3">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Delegate</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Urgent & Not Important</span>
|
||||
<span class="badge badge-amber">{{ counts.delegate }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.delegate %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
|
||||
{{ task.due_date.strftime('%b %-d') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Q4: Not Urgent + Not Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q4">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Eliminate</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Not Urgent & Not Important</span>
|
||||
<span class="badge badge-muted">{{ counts.eliminate }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.eliminate %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- X-axis labels -->
|
||||
<div class="eisenhower-x-spacer"></div>
|
||||
<div class="eisenhower-x-label">Urgent</div>
|
||||
<div class="eisenhower-x-label">Not Urgent</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var domainSel = document.getElementById('eis-domain');
|
||||
var projectSel = document.getElementById('eis-project');
|
||||
var currentProjectId = '{{ current_project_id }}';
|
||||
var form = document.getElementById('eis-filters');
|
||||
|
||||
domainSel.addEventListener('change', function() {
|
||||
var did = domainSel.value;
|
||||
if (!did) { form.submit(); return; }
|
||||
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(projects) {
|
||||
projectSel.innerHTML = '<option value="">All Projects</option>';
|
||||
projects.forEach(function(p) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
if (p.id === currentProjectId) opt.selected = true;
|
||||
projectSel.appendChild(opt);
|
||||
});
|
||||
form.submit();
|
||||
})
|
||||
.catch(function() { form.submit(); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
52
templates/file_preview.html
Normal file
52
templates/file_preview.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/files">Files</a>
|
||||
<span class="sep">/</span>
|
||||
{% if folder and folder != '/' %}
|
||||
<a href="/files?folder={{ folder }}">{{ folder }}</a>
|
||||
<span class="sep">/</span>
|
||||
{% endif %}
|
||||
<span>{{ item.original_filename }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.original_filename }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/files/{{ item.id }}/download" class="btn btn-primary btn-sm">Download</a>
|
||||
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="detail-meta-item">Folder: {{ folder }}</span>
|
||||
{% if item.mime_type %}<span class="detail-meta-item">Type: {{ item.mime_type }}</span>{% endif %}
|
||||
{% if item.size_bytes %}<span class="detail-meta-item">Size: {{ "%.1f"|format(item.size_bytes / 1024) }} KB</span>{% endif %}
|
||||
{% if item.description %}<span class="detail-meta-item">{{ item.description }}</span>{% endif %}
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">
|
||||
{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if can_preview %}
|
||||
<div class="card mt-3" style="padding: 16px;">
|
||||
{% if item.mime_type and item.mime_type.startswith('image/') %}
|
||||
<img src="/files/{{ item.id }}/serve" alt="{{ item.original_filename }}" style="max-width: 100%; height: auto; border-radius: var(--radius);">
|
||||
{% elif item.mime_type == 'application/pdf' %}
|
||||
<iframe src="/files/{{ item.id }}/serve" style="width: 100%; height: 600px; border: none; border-radius: var(--radius);"></iframe>
|
||||
{% elif item.mime_type and item.mime_type.startswith('text/') %}
|
||||
<iframe src="/files/{{ item.id }}/serve" style="width: 100%; height: 400px; border: 1px solid var(--border); border-radius: var(--radius); background: #fff;"></iframe>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">📄</div>
|
||||
<div class="empty-state-text">Preview not available for this file type</div>
|
||||
<a href="/files/{{ item.id }}/download" class="btn btn-primary">Download Instead</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
52
templates/file_upload.html
Normal file
52
templates/file_upload.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Upload File</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/files/upload" enctype="multipart/form-data">
|
||||
{% if context_type %}
|
||||
<input type="hidden" name="context_type" value="{{ context_type }}">
|
||||
<input type="hidden" name="context_id" value="{{ context_id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">File *</label>
|
||||
<input type="file" name="file" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Folder</label>
|
||||
<select name="folder" class="form-input">
|
||||
<option value="">/ (root)</option>
|
||||
{% for f in folders %}
|
||||
<option value="{{ f }}" {{ 'selected' if prefill_folder == f }}>{{ f }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">New Folder</label>
|
||||
<input type="text" name="new_folder" class="form-input" placeholder="Or create new folder...">
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<input type="text" name="description" class="form-input" placeholder="Optional description...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Upload</button>
|
||||
<a href="/files" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
91
templates/files.html
Normal file
91
templates/files.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Files<span class="page-count">{{ items|length }}</span></h1>
|
||||
<div class="flex gap-2">
|
||||
<form action="/files/sync" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-secondary">Sync Files</button>
|
||||
</form>
|
||||
<a href="/files/upload{{ '?folder=' ~ current_folder if current_folder }}{{ '?context_type=' ~ context_type ~ '&context_id=' ~ context_id if context_type }}" class="btn btn-primary">+ Upload File</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if sync_result and (sync_result.added > 0 or sync_result.removed > 0) %}
|
||||
<div class="flash-message" style="background: var(--accent-soft); border: 1px solid var(--accent); border-radius: var(--radius); padding: 8px 12px; margin-bottom: 16px; color: var(--text); font-size: 0.85rem;">
|
||||
Synced: {{ sync_result.added }} file{{ 's' if sync_result.added != 1 }} added, {{ sync_result.removed }} removed
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
|
||||
<label style="color: var(--muted); font-size: 0.85rem; white-space: nowrap;">Folder:</label>
|
||||
<select class="form-input" style="max-width: 280px; padding: 6px 10px; font-size: 0.85rem;" onchange="window.location.href=this.value">
|
||||
<option value="/files" {{ 'selected' if current_folder is none }}>All folders</option>
|
||||
<option value="/files?folder=" {{ 'selected' if current_folder is not none and current_folder == '' }}>/ (root)</option>
|
||||
{% for f in folders %}
|
||||
<option value="/files?folder={{ f }}" {{ 'selected' if current_folder == f }}>{% if '/' in f %} {{ f.split('/')[-1] }}{% else %}{{ f }}{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% set sort_base = '/files?' ~ ('folder=' ~ current_folder ~ '&' if current_folder is not none else '') %}
|
||||
|
||||
{% if items %}
|
||||
<div class="card" style="overflow-x: auto;">
|
||||
<table class="data-table" style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid var(--border); text-align: left;">
|
||||
<th style="padding: 10px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'path_desc' if current_sort == 'path' else 'path' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Path {{ '▲' if current_sort == 'path' else ('▼' if current_sort == 'path_desc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 10px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'name_desc' if current_sort == 'name' else 'name' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Name {{ '▲' if current_sort == 'name' else ('▼' if current_sort == 'name_desc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 10px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Type</th>
|
||||
<th style="padding: 10px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Size</th>
|
||||
<th style="padding: 10px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
<a href="{{ sort_base }}sort={{ 'date_asc' if current_sort == 'date' else 'date' }}" style="color: var(--muted); text-decoration: none;">
|
||||
Date {{ '▼' if current_sort == 'date' else ('▲' if current_sort == 'date_asc' else '') }}
|
||||
</a>
|
||||
</th>
|
||||
<th style="padding: 10px 12px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 10px 12px; color: var(--muted); font-size: 0.85rem;">{{ item.folder }}</td>
|
||||
<td style="padding: 10px 12px;">
|
||||
<a href="/files/{{ item.id }}/preview" style="color: var(--accent);">{{ item.original_filename }}</a>
|
||||
</td>
|
||||
<td style="padding: 10px 12px;">
|
||||
{% if item.mime_type %}<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding: 10px 12px; color: var(--muted); font-size: 0.85rem; white-space: nowrap;">
|
||||
{% if item.size_bytes %}{{ "%.1f"|format(item.size_bytes / 1024) }} KB{% endif %}
|
||||
</td>
|
||||
<td style="padding: 10px 12px; color: var(--muted); font-size: 0.85rem; white-space: nowrap;">
|
||||
{{ item.created_at.strftime('%Y-%m-%d') if item.created_at else '' }}
|
||||
</td>
|
||||
<td style="padding: 10px 12px; text-align: right; white-space: nowrap;">
|
||||
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
|
||||
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📁</div>
|
||||
<div class="empty-state-text">No files{{ ' in this folder' if current_folder is not none else ' uploaded yet' }}</div>
|
||||
<a href="/files/upload" class="btn btn-primary">Upload First File</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
109
templates/focus.html
Normal file
109
templates/focus.html
Normal file
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Daily Focus</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if total_estimated %}<span class="text-sm text-muted">~{{ total_estimated }}min estimated</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<a href="/focus?focus_date={{ (focus_date|string)[:10] }}" class="btn btn-ghost btn-sm">Today</a>
|
||||
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Focus items -->
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<div class="focus-item {{ 'completed' if item.completed }}">
|
||||
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||
<div class="row-check">
|
||||
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
|
||||
<label for="f-{{ item.id }}"></label>
|
||||
</div>
|
||||
</form>
|
||||
<span class="priority-dot priority-{{ item.priority }}"></span>
|
||||
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
|
||||
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
||||
{% if item.estimated_minutes %}<span class="focus-meta">~{{ item.estimated_minutes }}min</span>{% endif %}
|
||||
{% if item.due_date %}<span class="focus-meta">{{ item.due_date }}</span>{% endif %}
|
||||
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">×</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add task to focus -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header"><h2 class="card-title">Add to Focus</h2></div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 8px 12px; border-bottom: 1px solid var(--border);">
|
||||
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||
<select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="area_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Areas</option>
|
||||
{% for a in areas %}
|
||||
<option value="{{ a.id }}" {{ 'selected' if current_area_id == a.id|string }}>{{ a.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="project_id" class="filter-select" id="focus-project" onchange="this.form.submit()">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% for t in available_tasks[:15] %}
|
||||
<div class="list-row">
|
||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||
<span class="row-title">{{ t.title }}</span>
|
||||
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
||||
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
|
||||
<form action="/focus/add" method="post" style="display:inline">
|
||||
<input type="hidden" name="task_id" value="{{ t.id }}">
|
||||
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available tasks matching filters</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var domainSel = document.getElementById('focus-domain');
|
||||
var projectSel = document.getElementById('focus-project');
|
||||
var currentProjectId = '{{ current_project_id }}';
|
||||
var form = document.getElementById('focus-filters');
|
||||
|
||||
domainSel.addEventListener('change', function() {
|
||||
var did = domainSel.value;
|
||||
if (!did) { form.submit(); return; }
|
||||
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(projects) {
|
||||
projectSel.innerHTML = '<option value="">All Projects</option>';
|
||||
projects.forEach(function(p) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
if (p.id === currentProjectId) opt.selected = true;
|
||||
projectSel.appendChild(opt);
|
||||
});
|
||||
form.submit();
|
||||
})
|
||||
.catch(function() { form.submit(); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
32
templates/history.html
Normal file
32
templates/history.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Change History<span class="page-count">{{ items|length }}</span></h1>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/history">
|
||||
<select name="entity_type" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Types</option>
|
||||
{% for t in type_options %}
|
||||
<option value="{{ t.value }}" {{ 'selected' if current_type == t.value }}>{{ t.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-tag" style="min-width: 70px; text-align: center;">{{ item.type_label }}</span>
|
||||
<span class="row-title"><a href="{{ item.url }}">{{ item.label }}</a></span>
|
||||
<span class="status-badge {{ 'status-active' if item.action == 'created' else 'status-open' }}">{{ item.action }}</span>
|
||||
<span class="row-meta">{{ item.updated_at.strftime('%Y-%m-%d %H:%M') if item.updated_at else '' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-text">No recent changes</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
21
templates/link_form.html
Normal file
21
templates/link_form.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/links">Links</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Link' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Link' if item else 'New Link' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/links/' ~ item.id ~ '/edit' if item else '/links/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label class="form-label">Label *</label><input type="text" name="label" class="form-input" required value="{{ item.label if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">URL *</label><input type="url" name="url" class="form-input" required value="{{ item.url if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Domain</label>
|
||||
<select name="domain_id" class="form-select"><option value="">-- None --</option>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-group"><label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select"><option value="">-- None --</option>{% for p in projects %}<option value="{{ p.id }}" {{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>{{ p.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-group full-width"><label class="form-label">Tags</label><input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..." value="{{ item.tags|join(', ') if item and item.tags else '' }}"></div>
|
||||
<div class="form-group full-width"><label class="form-label">Description</label><textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea></div>
|
||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="/links" class="btn btn-secondary">Cancel</a></div>
|
||||
</div>
|
||||
</form></div>
|
||||
{% endblock %}
|
||||
25
templates/links.html
Normal file
25
templates/links.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Links<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/links/create" class="btn btn-primary">+ New Link</a>
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
||||
<span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span>
|
||||
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
||||
<span class="row-meta">{{ item.url[:50] }}{% if item.url|length > 50 %}...{% endif %}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/links/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/links/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-icon">🔗</div><div class="empty-state-text">No links yet</div><a href="/links/create" class="btn btn-primary">Add Link</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
132
templates/list_detail.html
Normal file
132
templates/list_detail.html
Normal file
@@ -0,0 +1,132 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
<a href="/lists">Lists</a>
|
||||
<span class="sep">/</span>
|
||||
{% if domain %}<span style="color: {{ domain.color or 'var(--accent)' }}">{{ domain.name }}</span><span class="sep">/</span>{% endif %}
|
||||
{% if project %}<a href="/projects/{{ project.id }}">{{ project.name }}</a><span class="sep">/</span>{% endif %}
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.name }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/lists/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="detail-meta-item">
|
||||
<span class="row-tag">{{ item.list_type }}</span>
|
||||
</span>
|
||||
{% if item.description %}
|
||||
<p class="text-secondary mt-1">{{ item.description }}</p>
|
||||
{% endif %}
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">
|
||||
{% for tag in item.tags %}
|
||||
<span class="row-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Add item form -->
|
||||
<form class="quick-add mt-3" action="/lists/{{ item.id }}/items/add" method="post">
|
||||
<input type="text" name="content" placeholder="Add item..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
|
||||
<!-- List items -->
|
||||
{% if list_items %}
|
||||
<div class="card mt-2">
|
||||
{% for li in list_items %}
|
||||
<div class="list-row {{ 'completed' if li.completed }}">
|
||||
{% if item.list_type == 'checklist' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="li-{{ li.id }}" {{ 'checked' if li.completed }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="li-{{ li.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if li.completed }}">
|
||||
{{ li.content }}
|
||||
</span>
|
||||
<div class="row-actions">
|
||||
<form action="/lists/{{ item.id }}/items/{{ li.id }}/delete" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Child items -->
|
||||
{% for child in child_map.get(li.id|string, []) %}
|
||||
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
|
||||
{% if item.list_type == 'checklist' %}
|
||||
<div class="row-check">
|
||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="li-{{ child.id }}" {{ 'checked' if child.completed }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="li-{{ child.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if child.completed }}">
|
||||
{{ child.content }}
|
||||
</span>
|
||||
<div class="row-actions">
|
||||
<form action="/lists/{{ item.id }}/items/{{ child.id }}/delete" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">☐</div>
|
||||
<div class="empty-state-text">No items yet. Add one above.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Contacts -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Contacts<span class="page-count">{{ contacts|length }}</span></h3>
|
||||
</div>
|
||||
<form action="/lists/{{ item.id }}/contacts/add" method="post" class="flex gap-2 items-end" style="padding: 12px; border-bottom: 1px solid var(--border);">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<select name="contact_id" class="form-select" required>
|
||||
<option value="">Select contact...</option>
|
||||
{% for c in all_contacts %}
|
||||
<option value="{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<input type="text" name="role" class="form-input" placeholder="Role (optional)">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
{% for c in contacts %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/contacts/{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</a></span>
|
||||
{% if c.role %}<span class="row-tag">{{ c.role }}</span>{% endif %}
|
||||
<div class="row-actions">
|
||||
<form action="/lists/{{ item.id }}/contacts/{{ c.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="padding: 12px; color: var(--muted); font-size: 0.85rem;">No contacts linked</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
84
templates/list_form.html
Normal file
84
templates/list_form.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/lists/' ~ item.id ~ '/edit' if item else '/lists/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" name="name" class="form-input" required
|
||||
value="{{ item.name if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
<option value="">Select domain...</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}"
|
||||
{{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>
|
||||
{{ d.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Area</label>
|
||||
<select name="area_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for a in areas %}
|
||||
<option value="{{ a.id }}"
|
||||
{{ 'selected' if item and item.area_id and item.area_id|string == a.id|string }}>
|
||||
{{ a.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}"
|
||||
{{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="list_type" class="form-select">
|
||||
<option value="checklist" {{ 'selected' if item and item.list_type == 'checklist' }}>Checklist</option>
|
||||
<option value="ordered" {{ 'selected' if item and item.list_type == 'ordered' }}>Ordered</option>
|
||||
<option value="reference" {{ 'selected' if item and item.list_type == 'reference' }}>Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create List' }}</button>
|
||||
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=lists' if prefill_task_id is defined and prefill_task_id else ('/lists/' ~ item.id if item else '/lists') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
94
templates/lists.html
Normal file
94
templates/lists.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Lists<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/lists/create" class="btn btn-primary">+ New List</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form class="filters-bar" method="get" action="/lists" id="list-filters">
|
||||
<select name="domain_id" class="filter-select" id="domain-filter" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="project_id" class="filter-select" id="project-filter" onchange="this.form.submit()">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
|
||||
<span class="row-meta">
|
||||
{{ item.completed_count }}/{{ item.item_count }} items
|
||||
</span>
|
||||
{% if item.item_count > 0 %}
|
||||
<div class="progress-bar" style="width: 80px;">
|
||||
<div class="progress-fill" style="width: {{ (item.completed_count / item.item_count * 100) if item.item_count > 0 else 0 }}%"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="row-tag">{{ item.list_type }}</span>
|
||||
{% if item.domain_name %}
|
||||
<span class="row-domain-tag" style="background: {{ item.domain_color or '#4F6EF7' }}22; color: {{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
|
||||
{% endif %}
|
||||
{% if item.project_name %}
|
||||
<span class="row-tag">{{ item.project_name }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/lists/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">☰</div>
|
||||
<div class="empty-state-text">No lists yet</div>
|
||||
<a href="/lists/create" class="btn btn-primary">Create First List</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var domainSel = document.getElementById('domain-filter');
|
||||
var projectSel = document.getElementById('project-filter');
|
||||
var currentProjectId = '{{ current_project_id }}';
|
||||
|
||||
domainSel.addEventListener('change', function() {
|
||||
var did = domainSel.value;
|
||||
if (!did) {
|
||||
// No domain filter - submit immediately, server returns all projects
|
||||
document.getElementById('list-filters').submit();
|
||||
return;
|
||||
}
|
||||
// Fetch projects for this domain
|
||||
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(projects) {
|
||||
projectSel.innerHTML = '<option value="">All Projects</option>';
|
||||
projects.forEach(function(p) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
if (p.id === currentProjectId) opt.selected = true;
|
||||
projectSel.appendChild(opt);
|
||||
});
|
||||
document.getElementById('list-filters').submit();
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('list-filters').submit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
237
templates/meeting_detail.html
Normal file
237
templates/meeting_detail.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/meetings">Meetings</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.title }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/meetings/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/meetings/{{ item.id }}/delete" method="post" data-confirm="Delete this meeting?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="detail-meta-item">{{ item.meeting_date }}</span>
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
{% if item.location %}<span class="detail-meta-item">{{ item.location }}</span>{% endif %}
|
||||
{% if item.start_at and item.end_at %}
|
||||
<span class="detail-meta-item">{{ item.start_at.strftime('%H:%M') }} - {{ item.end_at.strftime('%H:%M') }}</span>
|
||||
{% endif %}
|
||||
{% if projects %}
|
||||
{% for p in projects %}
|
||||
<span class="detail-meta-item"><a href="/projects/{{ p.id }}" style="color: {{ p.domain_color or 'var(--accent)' }};">{{ p.name }}</a></span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-strip">
|
||||
<a href="/meetings/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=links" class="tab-item {{ 'active' if tab == 'links' }}">Links{% if counts.links %} ({{ counts.links }}){% endif %}</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
||||
<a href="/meetings/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
||||
</div>
|
||||
|
||||
{% if tab == 'overview' %}
|
||||
<!-- Agenda -->
|
||||
{% if item.agenda %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><h3 class="card-title">Agenda</h3></div>
|
||||
<div class="detail-body" style="padding: 12px 16px;">{{ item.agenda }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Meeting Notes -->
|
||||
{% if item.notes_body %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><h3 class="card-title">Notes</h3></div>
|
||||
<div class="detail-body" style="padding: 12px 16px;">{{ item.notes_body }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Transcript -->
|
||||
{% if item.transcript %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><h3 class="card-title">Transcript</h3></div>
|
||||
<div class="detail-body" style="padding: 12px 16px; font-family: var(--font-mono); font-size: 0.82rem; white-space: pre-wrap;">{{ item.transcript }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Items -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Action Items<span class="page-count">{{ action_items|length }}</span></h3>
|
||||
</div>
|
||||
<form class="quick-add" action="/meetings/{{ item.id }}/action-item" method="post" style="border-bottom: 1px solid var(--border);">
|
||||
<input type="text" name="title" placeholder="Add action item..." required>
|
||||
<select name="domain_id" class="filter-select" required style="width: auto;">
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
{% for task in action_items %}
|
||||
<div class="list-row {{ 'completed' if task.status == 'done' }}">
|
||||
<div class="row-check">
|
||||
<form action="/tasks/{{ task.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="mt-{{ task.id }}" {{ 'checked' if task.status == 'done' }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="mt-{{ task.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ task.id }}">{{ task.title }}</a></span>
|
||||
{% if task.project_name %}<span class="row-tag">{{ task.project_name }}</span>{% endif %}
|
||||
<span class="status-badge status-{{ task.status }}">{{ task.status|replace('_', ' ') }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No action items yet</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Decisions -->
|
||||
{% if decisions %}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Decisions<span class="page-count">{{ decisions|length }}</span></h3></div>
|
||||
{% for dec in decisions %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ dec.id }}">{{ dec.title }}</a></span>
|
||||
<span class="status-badge status-{{ dec.status }}">{{ dec.status }}</span>
|
||||
<span class="row-tag">{{ dec.impact }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif tab == 'notes' %}
|
||||
<a href="/notes/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Note</a>
|
||||
{% for n in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/notes/{{ n.id }}">{{ n.title }}</a></span>
|
||||
<span class="row-meta">{{ n.updated_at.strftime('%Y-%m-%d') if n.updated_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No notes linked to this meeting</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'links' %}
|
||||
<a href="/links/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Link</a>
|
||||
{% for w in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
||||
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No links linked to this meeting</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'files' %}
|
||||
<a href="/files/upload?context_type=meeting&context_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ Upload File</a>
|
||||
{% for f in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/files/{{ f.id }}">{{ f.original_filename }}</a></span>
|
||||
<span class="row-meta">{{ f.created_at.strftime('%Y-%m-%d') if f.created_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No files attached to this meeting</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'lists' %}
|
||||
<a href="/lists/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New List</a>
|
||||
{% for l in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ l.id }}">{{ l.name }}</a></span>
|
||||
<span class="row-meta">{{ l.item_count }} items</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No lists linked to this meeting</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'decisions' %}
|
||||
<div class="card mb-4">
|
||||
<form action="/meetings/{{ item.id }}/decisions/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Decision</label>
|
||||
<select name="decision_id" class="form-select" required>
|
||||
<option value="">Select decision...</option>
|
||||
{% for d in all_decisions %}
|
||||
<option value="{{ d.id }}">{{ d.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Link</button>
|
||||
</form>
|
||||
</div>
|
||||
<a href="/decisions/create?meeting_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Decision</a>
|
||||
{% for d in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ d.id }}">{{ d.title }}</a></span>
|
||||
<span class="status-badge status-{{ d.status }}">{{ d.status }}</span>
|
||||
{% if d.impact %}<span class="row-tag">{{ d.impact }}</span>{% endif %}
|
||||
{% if d.decided_at %}<span class="row-meta">{{ d.decided_at.strftime('%Y-%m-%d') if d.decided_at else '' }}</span>{% endif %}
|
||||
<div class="row-actions">
|
||||
<form action="/meetings/{{ item.id }}/decisions/{{ d.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Unlink">Unlink</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No decisions linked to this meeting</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'processes' %}
|
||||
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
||||
|
||||
{% elif tab == 'contacts' %}
|
||||
<div class="card mb-4">
|
||||
<form action="/meetings/{{ item.id }}/contacts/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Contact</label>
|
||||
<select name="contact_id" class="form-select" required>
|
||||
<option value="">Select contact...</option>
|
||||
{% for c in all_contacts %}
|
||||
<option value="{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="attendee">Attendee</option>
|
||||
<option value="organizer">Organizer</option>
|
||||
<option value="optional">Optional</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for c in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/contacts/{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</a></span>
|
||||
{% if c.role %}<span class="row-tag">{{ c.role }}</span>{% endif %}
|
||||
<span class="row-meta">{{ c.linked_at.strftime('%Y-%m-%d') if c.linked_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/meetings/{{ item.id }}/contacts/{{ c.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No contacts linked to this meeting</div></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
102
templates/meeting_form.html
Normal file
102
templates/meeting_form.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/meetings/' ~ item.id ~ '/edit' if item else '/meetings/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" class="form-input" required
|
||||
value="{{ item.title if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Date *</label>
|
||||
<input type="date" name="meeting_date" class="form-input" required
|
||||
value="{{ item.meeting_date if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="scheduled" {{ 'selected' if item and item.status == 'scheduled' }}>Scheduled</option>
|
||||
<option value="completed" {{ 'selected' if item and item.status == 'completed' }}>Completed</option>
|
||||
<option value="cancelled" {{ 'selected' if item and item.status == 'cancelled' }}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Start Time</label>
|
||||
<input type="datetime-local" name="start_at" class="form-input"
|
||||
value="{{ item.start_at.strftime('%Y-%m-%dT%H:%M') if item and item.start_at else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">End Time</label>
|
||||
<input type="datetime-local" name="end_at" class="form-input"
|
||||
value="{{ item.end_at.strftime('%Y-%m-%dT%H:%M') if item and item.end_at else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Location</label>
|
||||
<input type="text" name="location" class="form-input" placeholder="Zoom, Google Meet, Room..."
|
||||
value="{{ item.location if item and item.location else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="">None</option>
|
||||
<option value="1" {{ 'selected' if item and item.priority == 1 }}>Critical</option>
|
||||
<option value="2" {{ 'selected' if item and item.priority == 2 }}>High</option>
|
||||
<option value="3" {{ 'selected' if item and item.priority == 3 }}>Normal</option>
|
||||
<option value="4" {{ 'selected' if item and item.priority == 4 }}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Series (Parent Meeting)</label>
|
||||
<select name="parent_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for m in parent_meetings %}
|
||||
<option value="{{ m.id }}" {{ 'selected' if item and item.parent_id and item.parent_id|string == m.id|string }}>
|
||||
{{ m.title }} ({{ m.meeting_date }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Agenda</label>
|
||||
<textarea name="agenda" class="form-textarea" rows="4">{{ item.agenda if item and item.agenda else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
{% if item %}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Transcript</label>
|
||||
<textarea name="transcript" class="form-textarea" rows="6">{{ item.transcript if item and item.transcript else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Meeting Notes</label>
|
||||
<textarea name="notes_body" class="form-textarea" rows="6">{{ item.notes_body if item and item.notes_body else '' }}</textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create Meeting' }}</button>
|
||||
<a href="{{ '/meetings/' ~ item.id if item else '/meetings' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
templates/meetings.html
Normal file
46
templates/meetings.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Meetings<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/meetings/create" class="btn btn-primary">+ New Meeting</a>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/meetings">
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="scheduled" {{ 'selected' if current_status == 'scheduled' }}>Scheduled</option>
|
||||
<option value="completed" {{ 'selected' if current_status == 'completed' }}>Completed</option>
|
||||
<option value="cancelled" {{ 'selected' if current_status == 'cancelled' }}>Cancelled</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span>
|
||||
<span class="row-meta">{{ item.meeting_date }}</span>
|
||||
{% if item.location %}
|
||||
<span class="row-tag">{{ item.location }}</span>
|
||||
{% endif %}
|
||||
{% if item.action_count %}
|
||||
<span class="row-meta">{{ item.action_count }} action{{ 's' if item.action_count != 1 }}</span>
|
||||
{% endif %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/meetings/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/meetings/{{ item.id }}/delete" method="post" data-confirm="Delete this meeting?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">📅</div>
|
||||
<div class="empty-state-text">No meetings yet</div>
|
||||
<a href="/meetings/create" class="btn btn-primary">Schedule First Meeting</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
26
templates/note_detail.html
Normal file
26
templates/note_detail.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
{% if domain %}<span style="color:{{ domain.color }}">{{ domain.name }}</span><span class="sep">/</span>{% endif %}
|
||||
{% if project %}<a href="/projects/{{ project.id }}">{{ project.name }}</a><span class="sep">/</span>{% endif %}
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="detail-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="detail-title">{{ item.title }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/notes/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/notes/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-danger btn-sm">Delete</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="detail-meta-item">Updated {{ item.updated_at.strftime('%Y-%m-%d %H:%M') if item.updated_at else '' }}</span>
|
||||
{% if item.tags %}{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if item.body %}
|
||||
<div class="card"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body)">{{ item.body }}</div></div>
|
||||
{% else %}
|
||||
<div class="card"><div class="text-muted" style="padding:20px">No content yet. <a href="/notes/{{ item.id }}/edit">Start writing</a></div></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
21
templates/note_form.html
Normal file
21
templates/note_form.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/notes">Notes</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Note' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Note' if item else 'New Note' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/notes/' ~ item.id ~ '/edit' if item else '/notes/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width"><label class="form-label">Title *</label><input type="text" name="title" class="form-input" required value="{{ item.title if item else '' }}"></div>
|
||||
<div class="form-group"><label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-group"><label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select"><option value="">-- None --</option>{% for p in projects %}<option value="{{ p.id }}" {{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>{{ p.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-group full-width"><label class="form-label">Content</label><textarea name="body" class="form-textarea" rows="15" style="font-family:var(--font-mono);font-size:0.88rem">{{ item.body if item and item.body else '' }}</textarea></div>
|
||||
<div class="form-group full-width"><label class="form-label">Tags</label><input type="text" name="tags" class="form-input" value="{{ item.tags|join(', ') if item and item.tags else '' }}"></div>
|
||||
<input type="hidden" name="content_format" value="rich">
|
||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="{{ '/notes/' ~ item.id if item else '/notes' }}" class="btn btn-secondary">Cancel</a></div>
|
||||
</div>
|
||||
</form></div>
|
||||
{% endblock %}
|
||||
25
templates/notes.html
Normal file
25
templates/notes.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Notes<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/notes/create" class="btn btn-primary">+ New Note</a>
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
|
||||
<span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span>
|
||||
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
|
||||
<span class="row-meta">{{ item.updated_at.strftime('%Y-%m-%d') if item.updated_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/notes/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/notes/{{ item.id }}/delete" method="post" data-confirm="Delete?" style="display:inline"><button class="btn btn-ghost btn-xs" style="color:var(--red)">Del</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-icon">📄</div><div class="empty-state-text">No notes yet</div><a href="/notes/create" class="btn btn-primary">Create Note</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
133
templates/process_run_detail.html
Normal file
133
templates/process_run_detail.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/processes">Processes</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/processes/{{ run.process_id_ref }}">{{ run.process_name }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ run.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ run.title }}</h1>
|
||||
<div class="flex gap-2">
|
||||
{% if run.status != 'completed' %}
|
||||
<form action="/processes/runs/{{ run.id }}/complete" method="post" data-confirm="Mark this run as complete?" style="display:inline">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Mark Complete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form action="/processes/runs/{{ run.id }}/delete" method="post" data-confirm="Delete this run?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="status-badge status-{{ run.status }}">{{ run.status|replace('_', ' ') }}</span>
|
||||
<span class="row-tag">{{ run.process_type }}</span>
|
||||
<span class="row-tag">{{ run.task_generation|replace('_', ' ') }}</span>
|
||||
{% if run.project_name %}<span class="detail-meta-item">{{ run.project_name }}</span>{% endif %}
|
||||
{% if run.contact_first %}<span class="detail-meta-item">{{ run.contact_first }} {{ run.contact_last or '' }}</span>{% endif %}
|
||||
{% if run.started_at %}<span class="detail-meta-item">Started {{ run.started_at.strftime('%Y-%m-%d') }}</span>{% endif %}
|
||||
{% if run.completed_at %}<span class="detail-meta-item">Completed {{ run.completed_at.strftime('%Y-%m-%d') }}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="card mt-3" style="padding: 16px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="font-weight: 600; font-size: 0.9rem;">Progress</span>
|
||||
<div style="flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: {{ (completed_steps / total_steps * 100)|int if total_steps > 0 else 0 }}%; height: 100%; background: var(--green); border-radius: 4px; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<span style="font-weight: 600; font-size: 0.9rem;">{{ completed_steps }}/{{ total_steps }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steps Checklist -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Steps</h3>
|
||||
</div>
|
||||
|
||||
{% for step in steps %}
|
||||
<div class="list-row {{ 'completed' if step.status == 'completed' }}" style="align-items: flex-start;">
|
||||
<div class="row-check">
|
||||
{% if step.status == 'completed' %}
|
||||
<form action="/processes/runs/{{ run.id }}/steps/{{ step.id }}/uncomplete" method="post" style="display:inline">
|
||||
<input type="checkbox" id="step-{{ step.id }}" checked onchange="this.form.submit()">
|
||||
<label for="step-{{ step.id }}"></label>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/processes/runs/{{ run.id }}/steps/{{ step.id }}/complete" method="post" style="display:inline" id="complete-form-{{ step.id }}">
|
||||
<input type="checkbox" id="step-{{ step.id }}" onchange="this.form.submit()">
|
||||
<label for="step-{{ step.id }}"></label>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="row-meta" style="min-width: 20px; font-weight: 600;">{{ loop.index }}</span>
|
||||
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if step.status == 'completed' }}">{{ step.title }}</span>
|
||||
</div>
|
||||
{% if step.instructions %}
|
||||
<div style="color: var(--muted); font-size: 0.82rem; margin: 4px 0 0 28px;">{{ step.instructions }}</div>
|
||||
{% endif %}
|
||||
{% if step.completed_at %}
|
||||
<div style="color: var(--green); font-size: 0.78rem; margin: 4px 0 0 28px;">
|
||||
Completed {{ step.completed_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if step.notes %}
|
||||
<div style="color: var(--muted); font-size: 0.82rem; margin: 2px 0 0 28px; font-style: italic;">{{ step.notes }}</div>
|
||||
{% endif %}
|
||||
{% if step.status != 'completed' %}
|
||||
<div style="margin: 6px 0 0 28px;">
|
||||
<button type="button" class="btn btn-ghost btn-xs" onclick="toggleNotes('{{ step.id }}')">Add Notes</button>
|
||||
<div id="notes-{{ step.id }}" style="display: none; margin-top: 4px;">
|
||||
<form action="/processes/runs/{{ run.id }}/steps/{{ step.id }}/complete" method="post" style="display: flex; gap: 6px; align-items: flex-end;">
|
||||
<input type="text" name="notes" class="form-input" placeholder="Completion notes..." style="flex: 1; height: 32px; font-size: 0.82rem;">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Complete with Notes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Show linked tasks for this step -->
|
||||
{% if step_tasks.get(step.id|string) %}
|
||||
<div style="margin: 6px 0 0 28px;">
|
||||
{% for task in step_tasks[step.id|string] %}
|
||||
<div style="display: inline-flex; align-items: center; gap: 4px; margin-right: 8px;">
|
||||
<span class="status-badge status-{{ task.status }}" style="font-size: 0.72rem;">{{ task.status }}</span>
|
||||
<a href="/tasks/{{ task.id }}" style="font-size: 0.82rem;">{{ task.title }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- All Generated Tasks -->
|
||||
{% if tasks %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Generated Tasks<span class="page-count">{{ tasks|length }}</span></h3>
|
||||
</div>
|
||||
{% for task in tasks %}
|
||||
<div class="list-row {{ 'completed' if task.status == 'done' }}">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ task.id }}">{{ task.title }}</a></span>
|
||||
{% if task.project_name %}<span class="row-tag">{{ task.project_name }}</span>{% endif %}
|
||||
<span class="status-badge status-{{ task.status }}">{{ task.status|replace('_', ' ') }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleNotes(stepId) {
|
||||
var el = document.getElementById('notes-' + stepId);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
46
templates/process_runs.html
Normal file
46
templates/process_runs.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">All Process Runs<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/processes" class="btn btn-secondary">Back to Processes</a>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/processes/runs">
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="not_started" {{ 'selected' if current_status == 'not_started' }}>Not Started</option>
|
||||
<option value="in_progress" {{ 'selected' if current_status == 'in_progress' }}>In Progress</option>
|
||||
<option value="completed" {{ 'selected' if current_status == 'completed' }}>Completed</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/processes/runs/{{ item.id }}">{{ item.title }}</a></span>
|
||||
<span class="row-tag">{{ item.process_name }}</span>
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
{% if item.total_steps > 0 %}
|
||||
<div class="row-meta" style="display: flex; align-items: center; gap: 6px;">
|
||||
<div style="width: 60px; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden;">
|
||||
<div style="width: {{ (item.completed_steps / item.total_steps * 100)|int }}%; height: 100%; background: var(--green); border-radius: 2px;"></div>
|
||||
</div>
|
||||
<span>{{ item.completed_steps }}/{{ item.total_steps }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.project_name %}
|
||||
<span class="row-tag">{{ item.project_name }}</span>
|
||||
{% endif %}
|
||||
<span class="row-meta">{{ item.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">▶</div>
|
||||
<div class="empty-state-text">No process runs yet</div>
|
||||
<a href="/processes" class="btn btn-primary">Go to Processes</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
52
templates/processes.html
Normal file
52
templates/processes.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Processes<span class="page-count">{{ items|length }}</span></h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/processes/runs" class="btn btn-secondary">All Runs</a>
|
||||
<a href="/processes/create" class="btn btn-primary">+ New Process</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/processes">
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="draft" {{ 'selected' if current_status == 'draft' }}>Draft</option>
|
||||
<option value="active" {{ 'selected' if current_status == 'active' }}>Active</option>
|
||||
<option value="archived" {{ 'selected' if current_status == 'archived' }}>Archived</option>
|
||||
</select>
|
||||
<select name="process_type" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Types</option>
|
||||
<option value="workflow" {{ 'selected' if current_type == 'workflow' }}>Workflow</option>
|
||||
<option value="checklist" {{ 'selected' if current_type == 'checklist' }}>Checklist</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/processes/{{ item.id }}">{{ item.name }}</a></span>
|
||||
<span class="row-tag">{{ item.process_type }}</span>
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<span class="row-meta">{{ item.step_count }} step{{ 's' if item.step_count != 1 }}</span>
|
||||
{% if item.category %}
|
||||
<span class="row-tag">{{ item.category }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<a href="/processes/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/processes/{{ item.id }}/delete" method="post" data-confirm="Delete this process?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">⚙</div>
|
||||
<div class="empty-state-text">No processes yet</div>
|
||||
<a href="/processes/create" class="btn btn-primary">Create First Process</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
183
templates/processes_detail.html
Normal file
183
templates/processes_detail.html
Normal file
@@ -0,0 +1,183 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/processes">Processes</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<h1 class="detail-title">{{ item.name }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/processes/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/processes/{{ item.id }}/delete" method="post" data-confirm="Delete this process?" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
|
||||
<span class="row-tag">{{ item.process_type }}</span>
|
||||
{% if item.category %}<span class="detail-meta-item">{{ item.category }}</span>{% endif %}
|
||||
<span class="detail-meta-item">{{ steps|length }} step{{ 's' if steps|length != 1 }}</span>
|
||||
<span class="detail-meta-item">Created {{ item.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
{% if item.tags %}
|
||||
<div class="mt-1">{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.description %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Description</h3></div>
|
||||
<div class="detail-body" style="padding: 12px 16px;">{{ item.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Steps<span class="page-count">{{ steps|length }}</span></h3>
|
||||
</div>
|
||||
|
||||
{% for step in steps %}
|
||||
<div class="list-row" style="align-items: flex-start;">
|
||||
<span class="row-meta" style="min-width: 28px; text-align: center; font-weight: 600;">{{ loop.index }}</span>
|
||||
<div style="flex: 1;">
|
||||
<span class="row-title">{{ step.title }}</span>
|
||||
{% if step.instructions %}
|
||||
<div style="color: var(--muted); font-size: 0.82rem; margin-top: 2px;">{{ step.instructions[:120] }}{{ '...' if step.instructions|length > 120 }}</div>
|
||||
{% endif %}
|
||||
{% if step.expected_output %}
|
||||
<div style="color: var(--muted); font-size: 0.82rem; margin-top: 2px;">Output: {{ step.expected_output[:80] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if step.estimated_days %}
|
||||
<span class="row-meta">{{ step.estimated_days }}d</span>
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
<button type="button" class="btn btn-ghost btn-xs" onclick="toggleEditStep('{{ step.id }}')">Edit</button>
|
||||
<form action="/processes/{{ item.id }}/steps/{{ step.id }}/delete" method="post" data-confirm="Delete this step?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inline edit form (hidden by default) -->
|
||||
<div id="edit-step-{{ step.id }}" style="display: none; border-bottom: 1px solid var(--border); padding: 12px 16px; background: var(--surface2);">
|
||||
<form action="/processes/{{ item.id }}/steps/{{ step.id }}/edit" method="post">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" class="form-input" value="{{ step.title }}" required>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Instructions</label>
|
||||
<textarea name="instructions" class="form-textarea" rows="2">{{ step.instructions or '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Expected Output</label>
|
||||
<input type="text" name="expected_output" class="form-input" value="{{ step.expected_output or '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Estimated Days</label>
|
||||
<input type="number" name="estimated_days" class="form-input" min="0" value="{{ step.estimated_days or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top: 8px;">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save Step</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleEditStep('{{ step.id }}')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Quick add step -->
|
||||
<form class="quick-add" action="/processes/{{ item.id }}/steps/add" method="post" style="border-top: 1px solid var(--border);">
|
||||
<input type="text" name="title" placeholder="Add a step..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Step</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Runs -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Runs<span class="page-count">{{ runs|length }}</span></h3>
|
||||
</div>
|
||||
|
||||
{% for run in runs %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/processes/runs/{{ run.id }}">{{ run.title }}</a></span>
|
||||
<span class="status-badge status-{{ run.status }}">{{ run.status|replace('_', ' ') }}</span>
|
||||
{% if run.total_steps > 0 %}
|
||||
<span class="row-meta">{{ run.completed_steps }}/{{ run.total_steps }} steps</span>
|
||||
{% endif %}
|
||||
{% if run.project_name %}
|
||||
<span class="row-tag">{{ run.project_name }}</span>
|
||||
{% endif %}
|
||||
<span class="row-meta">{{ run.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/processes/runs/{{ run.id }}/delete" method="post" data-confirm="Delete this run?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not runs %}
|
||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No runs yet</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Start Run Form -->
|
||||
{% if steps %}
|
||||
<div style="border-top: 1px solid var(--border); padding: 12px 16px;">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="document.getElementById('start-run-form').style.display = document.getElementById('start-run-form').style.display === 'none' ? 'block' : 'none'">+ Start Run</button>
|
||||
<div id="start-run-form" style="display: none; margin-top: 12px;">
|
||||
<form action="/processes/{{ item.id }}/runs/start" method="post">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Run Title *</label>
|
||||
<input type="text" name="title" class="form-input" required
|
||||
value="{{ item.name }} - Run">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Task Generation</label>
|
||||
<select name="task_generation" class="form-select">
|
||||
<option value="all_at_once">All at Once</option>
|
||||
<option value="step_by_step">Step by Step</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Contact</label>
|
||||
<select name="contact_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for c in contacts %}
|
||||
<option value="{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top: 8px;">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Start Run</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleEditStep(stepId) {
|
||||
var el = document.getElementById('edit-step-' + stepId);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
57
templates/processes_form.html
Normal file
57
templates/processes_form.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/processes/' ~ item.id ~ '/edit' if item else '/processes/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" name="name" class="form-input" required
|
||||
value="{{ item.name if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="process_type" class="form-select">
|
||||
<option value="checklist" {{ 'selected' if item and item.process_type == 'checklist' }}>Checklist</option>
|
||||
<option value="workflow" {{ 'selected' if item and item.process_type == 'workflow' }}>Workflow</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="draft" {{ 'selected' if item and item.status == 'draft' }}>Draft</option>
|
||||
<option value="active" {{ 'selected' if item and item.status == 'active' }}>Active</option>
|
||||
<option value="archived" {{ 'selected' if item and item.status == 'archived' }}>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category</label>
|
||||
<input type="text" name="category" class="form-input" placeholder="e.g. Onboarding, Publishing..."
|
||||
value="{{ item.category if item and item.category else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create Process' }}</button>
|
||||
<a href="{{ '/processes/' ~ item.id if item else '/processes' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
202
templates/project_detail.html
Normal file
202
templates/project_detail.html
Normal file
@@ -0,0 +1,202 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
{% if domain %}<span style="color: {{ domain.color or '#4F6EF7' }}">{{ domain.name }}</span><span class="sep">/</span>{% endif %}
|
||||
{% if area %}<span>{{ area.name }}</span><span class="sep">/</span>{% endif %}
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="detail-title">{{ item.name }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/projects/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/projects/{{ item.id }}/delete" method="post" data-confirm="Delete this project?" style="display:inline">
|
||||
<button class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
<span class="detail-meta-item"><span class="priority-dot priority-{{ item.priority }}"></span> P{{ item.priority }}</span>
|
||||
{% if item.target_date %}<span class="detail-meta-item">Target: {{ item.target_date }}</span>{% endif %}
|
||||
</div>
|
||||
<a href="/tasks/?project_id={{ item.id }}" class="stat-card-link" style="max-width: 300px; display: block; margin-top: 8px;">
|
||||
<div class="progress-bar"><div class="progress-fill" style="width: {{ progress }}%"></div></div>
|
||||
<div class="progress-text">{{ done_count }}/{{ task_count }} tasks complete ({{ progress }}%)</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if item.description %}
|
||||
<div class="card mb-4"><div class="detail-body">{{ item.description }}</div></div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-strip">
|
||||
<a href="/projects/{{ item.id }}?tab=tasks" class="tab-item {{ 'active' if tab == 'tasks' }}">Tasks ({{ tasks|length }})</a>
|
||||
<a href="/projects/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=links" class="tab-item {{ 'active' if tab == 'links' }}">Links{% if counts.links %} ({{ counts.links }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=meetings" class="tab-item {{ 'active' if tab == 'meetings' }}">Meetings{% if counts.meetings %} ({{ counts.meetings }}){% endif %}</a>
|
||||
<a href="/projects/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
||||
<a href="/projects/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
||||
</div>
|
||||
|
||||
{% if tab == 'tasks' %}
|
||||
<form class="quick-add" action="/tasks/quick-add" method="post">
|
||||
<input type="hidden" name="domain_id" value="{{ item.domain_id }}">
|
||||
<input type="hidden" name="project_id" value="{{ item.id }}">
|
||||
<input type="text" name="title" placeholder="Quick add task to this project..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
<a href="/tasks/create?domain_id={{ item.domain_id }}&project_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ Full Task Form</a>
|
||||
|
||||
{% for t in tasks %}
|
||||
<div class="list-row {{ 'completed' if t.status == 'done' }}">
|
||||
<div class="row-check">
|
||||
<form action="/tasks/{{ t.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="pt-{{ t.id }}" {{ 'checked' if t.status == 'done' }} onchange="this.form.submit()">
|
||||
<label for="pt-{{ t.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ t.id }}">{{ t.title }}</a></span>
|
||||
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
|
||||
<span class="status-badge status-{{ t.status }}">{{ t.status|replace('_', ' ') }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/tasks/{{ t.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No tasks yet</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'notes' %}
|
||||
<a href="/notes/create?domain_id={{ item.domain_id }}&project_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Note</a>
|
||||
{% for n in notes %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/notes/{{ n.id }}">{{ n.title }}</a></span>
|
||||
<span class="row-meta">{{ n.updated_at.strftime('%Y-%m-%d') if n.updated_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No notes yet</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'links' %}
|
||||
<a href="/links/create?domain_id={{ item.domain_id }}&project_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Link</a>
|
||||
{% for l in links %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="{{ l.url }}" target="_blank">{{ l.label }}</a></span>
|
||||
<span class="row-meta">{{ l.url[:50] }}{% if l.url|length > 50 %}...{% endif %}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/links/{{ l.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No links yet</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'files' %}
|
||||
<a href="/files/upload?context_type=project&context_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ Upload File</a>
|
||||
{% for f in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/files/{{ f.id }}">{{ f.original_filename }}</a></span>
|
||||
<span class="row-meta">{{ f.created_at.strftime('%Y-%m-%d') if f.created_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No files attached to this project</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'lists' %}
|
||||
<a href="/lists/create?domain_id={{ item.domain_id }}&project_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New List</a>
|
||||
{% for l in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ l.id }}">{{ l.name }}</a></span>
|
||||
<span class="row-meta">{{ l.item_count }} items</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No lists linked to this project</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'decisions' %}
|
||||
<a href="/decisions/create?project_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Decision</a>
|
||||
{% for d in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ d.id }}">{{ d.title }}</a></span>
|
||||
<span class="status-badge status-{{ d.status }}">{{ d.status }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No decisions linked to this project</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'meetings' %}
|
||||
<a href="/meetings/create" class="btn btn-ghost btn-sm mb-3">+ New Meeting</a>
|
||||
<div class="card mb-4">
|
||||
<form action="/projects/{{ item.id }}/meetings/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Meeting</label>
|
||||
<select name="meeting_id" class="form-select" required>
|
||||
<option value="">Select meeting...</option>
|
||||
{% for m in all_meetings %}
|
||||
<option value="{{ m.id }}">{{ m.title }} ({{ m.meeting_date }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for m in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/meetings/{{ m.id }}">{{ m.title }}</a></span>
|
||||
<span class="row-meta">{{ m.meeting_date }}</span>
|
||||
<span class="status-badge status-{{ m.status }}">{{ m.status }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/projects/{{ item.id }}/meetings/{{ m.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No meetings linked to this project</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'processes' %}
|
||||
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
||||
|
||||
{% elif tab == 'contacts' %}
|
||||
<div class="card mb-4">
|
||||
<form action="/projects/{{ item.id }}/contacts/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Contact</label>
|
||||
<select name="contact_id" class="form-select" required>
|
||||
<option value="">Select contact...</option>
|
||||
{% for c in all_contacts %}
|
||||
<option value="{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" name="role" class="form-input" placeholder="e.g. lead, contributor...">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for c in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/contacts/{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</a></span>
|
||||
{% if c.role %}<span class="row-tag">{{ c.role }}</span>{% endif %}
|
||||
<span class="row-meta">{{ c.linked_at.strftime('%Y-%m-%d') if c.linked_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/projects/{{ item.id }}/contacts/{{ c.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No contacts linked to this project</div></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
69
templates/project_form.html
Normal file
69
templates/project_form.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb"><a href="/projects">Projects</a><span class="sep">/</span><span>{{ 'Edit' if item else 'New Project' }}</span></div>
|
||||
<div class="page-header"><h1 class="page-title">{{ 'Edit Project' if item else 'New Project' }}</h1></div>
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/projects/' ~ item.id ~ '/edit' if item else '/projects/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" name="name" class="form-input" required value="{{ item.name if item else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Area</label>
|
||||
<select name="area_id" class="form-select">
|
||||
<option value="">-- None --</option>
|
||||
{% for a in areas %}
|
||||
<option value="{{ a.id }}" {{ 'selected' if (item and item.area_id and item.area_id|string == a.id|string) or (not item and prefill_area_id == a.id|string) }}>{{ a.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
{% for s in ['active', 'on_hold', 'completed', 'archived'] %}
|
||||
<option value="{{ s }}" {{ 'selected' if item and item.status == s }}>{{ s|replace('_', ' ')|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="1" {{ 'selected' if item and item.priority == 1 }}>Critical</option>
|
||||
<option value="2" {{ 'selected' if item and item.priority == 2 }}>High</option>
|
||||
<option value="3" {{ 'selected' if (item and item.priority == 3) or not item }}>Normal</option>
|
||||
<option value="4" {{ 'selected' if item and item.priority == 4 }}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Start Date</label>
|
||||
<input type="date" name="start_date" class="form-input" value="{{ item.start_date if item and item.start_date else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Target Date</label>
|
||||
<input type="date" name="target_date" class="form-input" value="{{ item.target_date if item and item.target_date else '' }}">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Tags (comma-separated)</label>
|
||||
<input type="text" name="tags" class="form-input" value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create Project' }}</button>
|
||||
<a href="{{ '/projects/' ~ item.id if item else '/projects' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
templates/projects.html
Normal file
48
templates/projects.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Projects<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/projects/create" class="btn btn-primary">+ New Project</a>
|
||||
</div>
|
||||
|
||||
<form class="filters-bar" method="get" action="/projects">
|
||||
<select name="domain_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="status" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
{% for s in ['active', 'on_hold', 'completed', 'archived'] %}
|
||||
<option value="{{ s }}" {{ 'selected' if current_status == s }}>{{ s|replace('_', ' ')|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-domain-tag" style="background: {{ item.domain_color or '#4F6EF7' }}22; color: {{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
|
||||
<span class="row-title"><a href="/projects/{{ item.id }}">{{ item.name }}</a></span>
|
||||
{% if item.area_name %}<span class="row-meta">{{ item.area_name }}</span>{% endif %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
<div style="width: 80px;">
|
||||
<div class="progress-bar"><div class="progress-fill" style="width: {{ item.progress }}%"></div></div>
|
||||
<div class="progress-text">{{ item.done_count }}/{{ item.task_count }}</div>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<a href="/projects/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📂</div>
|
||||
<div class="empty-state-text">No projects found</div>
|
||||
<a href="/projects/create" class="btn btn-primary">Create First Project</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
37
templates/search.html
Normal file
37
templates/search.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Search</h1>
|
||||
</div>
|
||||
|
||||
<form class="search-page-form" method="get" action="/search">
|
||||
<input type="text" name="q" value="{{ query }}" class="form-input" placeholder="Search tasks, projects, notes, contacts..." autofocus style="max-width: 600px;">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<p class="text-muted mt-2">{{ results|length }} result{{ 's' if results|length != 1 }} for "{{ query }}"</p>
|
||||
{% endif %}
|
||||
|
||||
{% if results %}
|
||||
<div class="card mt-3">
|
||||
{% for item in results %}
|
||||
<div class="list-row">
|
||||
<span class="search-type-badge search-type-{{ item.type }}">{{ item.type_label }}</span>
|
||||
<span class="row-title"><a href="{{ item.url }}">{{ item.name }}</a></span>
|
||||
{% if item.context %}
|
||||
<span class="row-meta">{{ item.context }}</span>
|
||||
{% endif %}
|
||||
{% if item.status %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif query %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<div class="empty-state-text">No results found for "{{ query }}"</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
196
templates/task_detail.html
Normal file
196
templates/task_detail.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
{% if domain %}<a href="/tasks?domain_id={{ item.domain_id }}">{{ domain.name }}</a><span class="sep">/</span>{% endif %}
|
||||
{% if project %}<a href="/projects/{{ project.id }}">{{ project.name }}</a><span class="sep">/</span>{% endif %}
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="detail-title">{{ item.title }}</h1>
|
||||
<div class="flex gap-2">
|
||||
{% if item.status not in ['done', 'cancelled'] %}
|
||||
{% if running_task_id and item.id|string == running_task_id %}
|
||||
<form action="/time/stop" method="post" style="display:inline">
|
||||
<button class="btn btn-sm timer-detail-btn timer-detail-stop" title="Stop timer">■ Stop Timer</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/time/start" method="post" style="display:inline">
|
||||
<input type="hidden" name="task_id" value="{{ item.id }}">
|
||||
<button class="btn btn-sm timer-detail-btn timer-detail-play" title="Start timer">▶ Start Timer</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="/tasks/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
|
||||
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||
<button class="btn {{ 'btn-secondary' if item.status == 'done' else 'btn-primary' }} btn-sm">
|
||||
{{ 'Reopen' if item.status == 'done' else 'Complete' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta mt-2">
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
<span class="detail-meta-item"><span class="priority-dot priority-{{ item.priority }}"></span> P{{ item.priority }}</span>
|
||||
{% if domain %}<span class="row-domain-tag" style="background: {{ domain.color or '#4F6EF7' }}22; color: {{ domain.color or '#4F6EF7' }}">{{ domain.name }}</span>{% endif %}
|
||||
{% if project %}<span class="row-tag">{{ project.name }}</span>{% endif %}
|
||||
{% if item.due_date %}<span class="detail-meta-item">Due: {{ item.due_date }}</span>{% endif %}
|
||||
{% if item.context %}<span class="detail-meta-item">@{{ item.context }}</span>{% endif %}
|
||||
{% if item.estimated_minutes %}<span class="detail-meta-item">~{{ item.estimated_minutes }}min</span>{% endif %}
|
||||
{% if item.energy_required %}<span class="detail-meta-item">Energy: {{ item.energy_required }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item.description %}
|
||||
<div class="card mb-4">
|
||||
<div class="detail-body">{{ item.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.tags %}
|
||||
<div class="flex gap-2 mb-4">
|
||||
{% for tag in item.tags %}<span class="row-tag">{{ tag }}</span>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-strip">
|
||||
<a href="/tasks/{{ item.id }}?tab=overview" class="tab-item {{ 'active' if tab == 'overview' }}">Overview{% if counts.overview %} ({{ counts.overview }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=notes" class="tab-item {{ 'active' if tab == 'notes' }}">Notes{% if counts.notes %} ({{ counts.notes }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=links" class="tab-item {{ 'active' if tab == 'links' }}">Links{% if counts.links %} ({{ counts.links }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=files" class="tab-item {{ 'active' if tab == 'files' }}">Files{% if counts.files %} ({{ counts.files }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=lists" class="tab-item {{ 'active' if tab == 'lists' }}">Lists{% if counts.lists %} ({{ counts.lists }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=decisions" class="tab-item {{ 'active' if tab == 'decisions' }}">Decisions{% if counts.decisions %} ({{ counts.decisions }}){% endif %}</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=processes" class="tab-item {{ 'active' if tab == 'processes' }}">Processes</a>
|
||||
<a href="/tasks/{{ item.id }}?tab=contacts" class="tab-item {{ 'active' if tab == 'contacts' }}">Contacts{% if counts.contacts %} ({{ counts.contacts }}){% endif %}</a>
|
||||
</div>
|
||||
|
||||
{% if tab == 'overview' %}
|
||||
{% if parent %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-title text-sm">Parent Task</div>
|
||||
<a href="/tasks/{{ parent.id }}">{{ parent.title }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Subtasks<span class="page-count">{{ subtasks|length }}</span></h2>
|
||||
<a href="/tasks/create?parent_id={{ item.id }}&domain_id={{ item.domain_id }}&project_id={{ item.project_id or '' }}" class="btn btn-ghost btn-sm">+ Add Subtask</a>
|
||||
</div>
|
||||
{% for sub in subtasks %}
|
||||
<div class="list-row {{ 'completed' if sub.status == 'done' }}">
|
||||
<div class="row-check">
|
||||
<form action="/tasks/{{ sub.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="sub-{{ sub.id }}" {{ 'checked' if sub.status == 'done' }} onchange="this.form.submit()">
|
||||
<label for="sub-{{ sub.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
<span class="priority-dot priority-{{ sub.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ sub.id }}">{{ sub.title }}</a></span>
|
||||
<span class="status-badge status-{{ sub.status }}">{{ sub.status|replace('_', ' ') }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm text-muted" style="padding: 12px;">No subtasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% elif tab == 'notes' %}
|
||||
<a href="/notes/create?task_id={{ item.id }}&domain_id={{ item.domain_id }}" class="btn btn-ghost btn-sm mb-3">+ New Note</a>
|
||||
{% for n in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/notes/{{ n.id }}">{{ n.title }}</a></span>
|
||||
<span class="row-meta">{{ n.updated_at.strftime('%Y-%m-%d') if n.updated_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No notes linked to this task</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'links' %}
|
||||
<a href="/links/create?task_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Link</a>
|
||||
{% for w in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="{{ w.url }}" target="_blank">{{ w.label }}</a></span>
|
||||
<span class="row-meta">{{ w.url[:50] }}{% if w.url|length > 50 %}...{% endif %}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No links linked to this task</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'files' %}
|
||||
<a href="/files/upload?context_type=task&context_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ Upload File</a>
|
||||
{% for f in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/files/{{ f.id }}">{{ f.original_filename }}</a></span>
|
||||
<span class="row-meta">{{ f.created_at.strftime('%Y-%m-%d') if f.created_at else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No files attached to this task</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'lists' %}
|
||||
<a href="/lists/create?task_id={{ item.id }}&domain_id={{ item.domain_id }}" class="btn btn-ghost btn-sm mb-3">+ New List</a>
|
||||
{% for l in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/lists/{{ l.id }}">{{ l.name }}</a></span>
|
||||
<span class="row-meta">{{ l.item_count }} items</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No lists linked to this task</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'decisions' %}
|
||||
<a href="/decisions/create?task_id={{ item.id }}" class="btn btn-ghost btn-sm mb-3">+ New Decision</a>
|
||||
{% for d in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/decisions/{{ d.id }}">{{ d.title }}</a></span>
|
||||
<span class="status-badge status-{{ d.status }}">{{ d.status }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No decisions linked to this task</div></div>
|
||||
{% endfor %}
|
||||
|
||||
{% elif tab == 'processes' %}
|
||||
<div class="empty-state"><div class="empty-state-text">Process management coming soon</div></div>
|
||||
|
||||
{% elif tab == 'contacts' %}
|
||||
<div class="card mb-4">
|
||||
<form action="/tasks/{{ item.id }}/contacts/add" method="post" class="flex gap-2 items-end" style="padding: 12px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Contact</label>
|
||||
<select name="contact_id" class="form-select" required>
|
||||
<option value="">Select contact...</option>
|
||||
{% for c in all_contacts %}
|
||||
<option value="{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" name="role" class="form-input" placeholder="e.g. reviewer, assignee...">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for c in tab_data %}
|
||||
<div class="list-row">
|
||||
<span class="row-title"><a href="/contacts/{{ c.id }}">{{ c.first_name }} {{ c.last_name or '' }}</a></span>
|
||||
{% if c.role %}<span class="row-tag">{{ c.role }}</span>{% endif %}
|
||||
<span class="row-meta">{{ c.linked_at.strftime('%Y-%m-%d') if c.linked_at else '' }}</span>
|
||||
<div class="row-actions">
|
||||
<form action="/tasks/{{ item.id }}/contacts/{{ c.id }}/remove" method="post" style="display:inline">
|
||||
<button class="btn btn-ghost btn-xs" title="Remove">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-state-text">No contacts linked to this task</div></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="text-xs text-muted mt-4">
|
||||
Created {{ item.created_at.strftime('%Y-%m-%d %H:%M') if item.created_at else '' }}
|
||||
{% if item.completed_at %} | Completed {{ item.completed_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
119
templates/task_form.html
Normal file
119
templates/task_form.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="breadcrumb">
|
||||
<a href="/tasks">Tasks</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ 'Edit' if item else 'New Task' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ 'Edit Task' if item else 'New Task' }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/tasks/' ~ item.id ~ '/edit' if item else '/tasks/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" class="form-input" required
|
||||
value="{{ item.title if item else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}"
|
||||
{{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>
|
||||
{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project_id" class="form-select">
|
||||
<option value="">-- No Project --</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}"
|
||||
{{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>
|
||||
{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="1" {{ 'selected' if item and item.priority == 1 }}>1 - Critical</option>
|
||||
<option value="2" {{ 'selected' if item and item.priority == 2 }}>2 - High</option>
|
||||
<option value="3" {{ 'selected' if (item and item.priority == 3) or not item }}>3 - Normal</option>
|
||||
<option value="4" {{ 'selected' if item and item.priority == 4 }}>4 - Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
{% for s in ['open', 'in_progress', 'blocked', 'done', 'cancelled'] %}
|
||||
<option value="{{ s }}" {{ 'selected' if item and item.status == s }}>{{ s|replace('_', ' ')|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Due Date</label>
|
||||
<input type="date" name="due_date" class="form-input"
|
||||
value="{{ item.due_date if item and item.due_date else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Context</label>
|
||||
<select name="context" class="form-select">
|
||||
<option value="">-- None --</option>
|
||||
{% for ct in context_types %}
|
||||
<option value="{{ ct.value }}"
|
||||
{{ 'selected' if item and item.context == ct.value }}>{{ ct.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Estimated Minutes</label>
|
||||
<input type="number" name="estimated_minutes" class="form-input" min="0"
|
||||
value="{{ item.estimated_minutes if item and item.estimated_minutes else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Energy Required</label>
|
||||
<select name="energy_required" class="form-select">
|
||||
<option value="">-- None --</option>
|
||||
<option value="high" {{ 'selected' if item and item.energy_required == 'high' }}>High</option>
|
||||
<option value="medium" {{ 'selected' if item and item.energy_required == 'medium' }}>Medium</option>
|
||||
<option value="low" {{ 'selected' if item and item.energy_required == 'low' }}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Tags (comma-separated)</label>
|
||||
<input type="text" name="tags" class="form-input"
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
|
||||
{% if prefill_parent_id %}
|
||||
<input type="hidden" name="parent_id" value="{{ prefill_parent_id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create Task' }}</button>
|
||||
<a href="{{ '/tasks/' ~ item.id if item else '/tasks' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
105
templates/tasks.html
Normal file
105
templates/tasks.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">All Tasks<span class="page-count">{{ items|length }}</span></h1>
|
||||
<a href="/tasks/create" class="btn btn-primary">+ New Task</a>
|
||||
</div>
|
||||
|
||||
<!-- Quick Add -->
|
||||
<form class="quick-add" action="/tasks/quick-add" method="post">
|
||||
<input type="text" name="title" placeholder="Quick add task..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
|
||||
<!-- Filters -->
|
||||
<form class="filters-bar" method="get" action="/tasks">
|
||||
<select name="status" class="filter-select" data-auto-submit onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="open" {{ 'selected' if current_status == 'open' }}>Open</option>
|
||||
<option value="in_progress" {{ 'selected' if current_status == 'in_progress' }}>In Progress</option>
|
||||
<option value="blocked" {{ 'selected' if current_status == 'blocked' }}>Blocked</option>
|
||||
<option value="done" {{ 'selected' if current_status == 'done' }}>Done</option>
|
||||
</select>
|
||||
<select name="priority" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Priorities</option>
|
||||
<option value="1" {{ 'selected' if current_priority == '1' }}>Critical</option>
|
||||
<option value="2" {{ 'selected' if current_priority == '2' }}>High</option>
|
||||
<option value="3" {{ 'selected' if current_priority == '3' }}>Normal</option>
|
||||
<option value="4" {{ 'selected' if current_priority == '4' }}>Low</option>
|
||||
</select>
|
||||
<select name="domain_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Domains</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="project_id" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in projects %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="sort" class="filter-select" onchange="this.form.submit()">
|
||||
<option value="sort_order" {{ 'selected' if current_sort == 'sort_order' }}>Manual Order</option>
|
||||
<option value="priority" {{ 'selected' if current_sort == 'priority' }}>Priority</option>
|
||||
<option value="due_date" {{ 'selected' if current_sort == 'due_date' }}>Due Date</option>
|
||||
<option value="created_at" {{ 'selected' if current_sort == 'created_at' }}>Newest</option>
|
||||
<option value="title" {{ 'selected' if current_sort == 'title' }}>Title</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<!-- Task List -->
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}">
|
||||
<div class="row-check">
|
||||
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}
|
||||
onchange="this.form.submit()">
|
||||
<label for="check-{{ item.id }}"></label>
|
||||
</form>
|
||||
</div>
|
||||
{% if item.status not in ['done', 'cancelled'] %}
|
||||
<div class="row-timer">
|
||||
{% if running_task_id and item.id|string == running_task_id %}
|
||||
<form action="/time/stop" method="post" style="display:inline">
|
||||
<button type="submit" class="timer-btn timer-btn-stop" title="Stop timer">■</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/time/start" method="post" style="display:inline">
|
||||
<input type="hidden" name="task_id" value="{{ item.id }}">
|
||||
<button type="submit" class="timer-btn timer-btn-play" title="Start timer">▶</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="priority-dot priority-{{ item.priority }}"></span>
|
||||
<span class="row-title"><a href="/tasks/{{ item.id }}">{{ item.title }}</a></span>
|
||||
{% if item.project_name %}
|
||||
<span class="row-tag">{{ item.project_name }}</span>
|
||||
{% endif %}
|
||||
{% if item.domain_name %}
|
||||
<span class="row-domain-tag" style="background: {{ item.domain_color or '#4F6EF7' }}22; color: {{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
|
||||
{% endif %}
|
||||
{% if item.due_date %}
|
||||
<span class="row-meta {{ 'overdue' if item.due_date|string < now_date|default('9999') }}">{{ item.due_date }}</span>
|
||||
{% endif %}
|
||||
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/tasks/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/tasks/{{ item.id }}/delete" method="post" data-confirm="Delete this task?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">☑</div>
|
||||
<div class="empty-state-text">No tasks found</div>
|
||||
<a href="/tasks/create" class="btn btn-primary">Create First Task</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
76
templates/time_budgets.html
Normal file
76
templates/time_budgets.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Time Budgets <span class="page-count">({{ count }})</span></h1>
|
||||
</div>
|
||||
<a href="/time-budgets/create" class="btn btn-primary">+ New Budget</a>
|
||||
</div>
|
||||
|
||||
{% if overcommitted %}
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong>Overcommitted!</strong> Your budgets total {{ "%.1f"|format(total_budgeted) }} hours/week, which exceeds the 168 hours available.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_budgets %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<span class="card-title">This Week's Budget vs Actual</span>
|
||||
<span class="text-muted text-sm">{{ "%.1f"|format(total_budgeted) }}h budgeted total</span>
|
||||
</div>
|
||||
|
||||
{% for b in current_budgets %}
|
||||
<div class="list-row" style="flex-wrap: wrap; gap: 8px;">
|
||||
<span class="domain-dot" style="background: {{ b.domain_color or '#4F6EF7' }}; flex-shrink: 0;"></span>
|
||||
<div class="row-title" style="min-width: 120px;">
|
||||
{{ b.domain_name }}
|
||||
</div>
|
||||
<div style="flex: 2; min-width: 200px; display: flex; align-items: center; gap: 8px;">
|
||||
<div class="progress-bar" style="flex: 1; height: 8px;">
|
||||
<div class="progress-fill" style="width: {{ [b.pct, 100] | min }}%; {{ 'background: var(--red);' if b.pct > 100 }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="row-meta" style="min-width: 100px; text-align: right;">
|
||||
<strong>{{ b.actual_hours }}h</strong> / {{ b.weekly_hours_float }}h
|
||||
</span>
|
||||
<span class="row-meta" style="min-width: 40px; text-align: right; {{ 'color: var(--red); font-weight: 600;' if b.pct > 100 else ('color: var(--green);' if b.pct >= 80 else '') }}">
|
||||
{{ b.pct }}%
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if all_budgets %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">All Budgets</span>
|
||||
</div>
|
||||
|
||||
{% for b in all_budgets %}
|
||||
<div class="list-row">
|
||||
<span class="domain-dot" style="background: {{ b.domain_color or '#4F6EF7' }}; flex-shrink: 0;"></span>
|
||||
<div class="row-title">
|
||||
{{ b.domain_name }}
|
||||
</div>
|
||||
<span class="row-meta">{{ b.weekly_hours }}h / week</span>
|
||||
<span class="row-meta">from {{ b.effective_from.strftime('%b %-d, %Y') if b.effective_from else '—' }}</span>
|
||||
<div class="row-actions">
|
||||
<a href="/time-budgets/{{ b.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form method="POST" action="/time-budgets/{{ b.id }}/delete" data-confirm="Delete this budget?">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red);">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⏱</div>
|
||||
<div class="empty-state-text">No time budgets defined</div>
|
||||
<a href="/time-budgets/create" class="btn btn-primary">Create a Budget</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
47
templates/time_budgets_form.html
Normal file
47
templates/time_budgets_form.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ 'Edit Time Budget' if budget else 'New Time Budget' }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="POST" action="{{ '/time-budgets/' ~ budget.id ~ '/edit' if budget else '/time-budgets/create' }}">
|
||||
<div class="form-grid" style="grid-template-columns: 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Domain *</label>
|
||||
<select name="domain_id" class="form-select" required>
|
||||
<option value="">Select domain...</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.id }}" {{ 'selected' if budget and budget.domain_id|string == d.id|string }}>
|
||||
{{ d.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Weekly Hours *</label>
|
||||
<input type="number" name="weekly_hours" class="form-input"
|
||||
value="{{ budget.weekly_hours if budget else '' }}"
|
||||
min="0" max="168" step="0.5" required
|
||||
placeholder="e.g. 10">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Effective From *</label>
|
||||
<input type="date" name="effective_from" class="form-input"
|
||||
value="{{ budget.effective_from.strftime('%Y-%m-%d') if budget and budget.effective_from else '' }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if budget else 'Create Budget' }}</button>
|
||||
<a href="/time-budgets" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
155
templates/time_entries.html
Normal file
155
templates/time_entries.html
Normal file
@@ -0,0 +1,155 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Time Log</h1>
|
||||
<div class="text-muted text-sm mt-1">
|
||||
{{ entries | length }} entries
|
||||
· {{ (total_minutes / 60) | round(1) }}h total
|
||||
(last {{ days }} days)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Running timer banner -->
|
||||
{% if running %}
|
||||
<div class="card mb-3" style="border-color: var(--green); background: var(--green-soft);">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 10px; height: 10px; border-radius: 50%; background: var(--green); animation: pulse 1.5s infinite;"></div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; color: var(--text);">
|
||||
Timer running: {{ running.task_title }}
|
||||
</div>
|
||||
{% if running.project_name %}
|
||||
<div class="text-muted text-sm">{{ running.project_name }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 700; font-family: var(--font-mono); color: var(--green);"
|
||||
id="running-banner-elapsed" data-start="{{ running.start_at.isoformat() }}">
|
||||
--:--
|
||||
</div>
|
||||
<form method="POST" action="/time/stop">
|
||||
<input type="hidden" name="entry_id" value="{{ running.id }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Stop</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar">
|
||||
<a href="/time?days=1" class="btn {{ 'btn-primary' if days == 1 else 'btn-secondary' }} btn-sm">Today</a>
|
||||
<a href="/time?days=7" class="btn {{ 'btn-primary' if days == 7 else 'btn-secondary' }} btn-sm">7 Days</a>
|
||||
<a href="/time?days=30" class="btn {{ 'btn-primary' if days == 30 else 'btn-secondary' }} btn-sm">30 Days</a>
|
||||
<a href="/time?days=90" class="btn {{ 'btn-primary' if days == 90 else 'btn-secondary' }} btn-sm">90 Days</a>
|
||||
</div>
|
||||
|
||||
<!-- Daily totals summary -->
|
||||
{% if daily_totals %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-title mb-2">Daily Summary</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||||
{% for day, mins in daily_totals | dictsort(reverse=true) %}
|
||||
<div style="text-align: center; padding: 8px 14px; background: var(--surface2); border-radius: var(--radius); min-width: 80px;">
|
||||
<div style="font-size: 1.1rem; font-weight: 700;">{{ (mins / 60) | round(1) }}h</div>
|
||||
<div class="text-muted text-xs">{{ day }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Time entries -->
|
||||
{% if entries %}
|
||||
<div class="card">
|
||||
{% set current_date = namespace(value='') %}
|
||||
{% for entry in entries %}
|
||||
{% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %}
|
||||
{% if entry_date != current_date.value %}
|
||||
<div style="padding: 10px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
|
||||
{{ entry_date }}
|
||||
</div>
|
||||
{% set current_date.value = entry_date %}
|
||||
{% endif %}
|
||||
|
||||
<div class="list-row">
|
||||
{% if entry.end_at is none %}
|
||||
<div style="width: 10px; height: 10px; border-radius: 50%; background: var(--green); animation: pulse 1.5s infinite; flex-shrink: 0;"></div>
|
||||
{% endif %}
|
||||
<div class="row-title">
|
||||
<a href="/tasks/{{ entry.task_id }}">{{ entry.task_title }}</a>
|
||||
</div>
|
||||
{% if entry.project_name %}
|
||||
<span class="row-tag">{{ entry.project_name }}</span>
|
||||
{% endif %}
|
||||
{% if entry.domain_name %}
|
||||
<span class="row-domain-tag" style="background: {{ entry.domain_color or 'var(--accent)' }}20; color: {{ entry.domain_color or 'var(--accent)' }};">
|
||||
{{ entry.domain_name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span style="font-family: var(--font-mono); font-size: 0.82rem; color: var(--text-secondary); min-width: 50px; text-align: right;">
|
||||
{% if entry.end_at is none %}
|
||||
<span style="color: var(--green);">running</span>
|
||||
{% elif entry.duration_minutes %}
|
||||
{% if entry.duration_minutes >= 60 %}
|
||||
{{ (entry.duration_minutes / 60) | int }}h {{ entry.duration_minutes % 60 }}m
|
||||
{% else %}
|
||||
{{ entry.duration_minutes }}m
|
||||
{% endif %}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="row-meta" style="min-width: 100px; text-align: right;">
|
||||
{% if entry.start_at %}
|
||||
{{ entry.start_at.strftime('%-I:%M %p') }}
|
||||
{% if entry.end_at %}
|
||||
- {{ entry.end_at.strftime('%-I:%M %p') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<div class="row-actions">
|
||||
<form method="POST" action="/time/{{ entry.id }}/delete" data-confirm="Delete this time entry?">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red);">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⏱</div>
|
||||
<div class="empty-state-text">No time entries in the last {{ days }} days</div>
|
||||
<div class="text-muted text-sm">Start a timer from any task to begin tracking time</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Update running banner elapsed time
|
||||
(function() {
|
||||
const el = document.getElementById('running-banner-elapsed');
|
||||
if (!el) return;
|
||||
const startAt = new Date(el.dataset.start);
|
||||
function update() {
|
||||
const now = new Date();
|
||||
const secs = Math.floor((now - startAt) / 1000);
|
||||
const h = Math.floor(secs / 3600);
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
const s = secs % 60;
|
||||
el.textContent = (h > 0 ? h + ':' : '') +
|
||||
String(m).padStart(2, '0') + ':' +
|
||||
String(s).padStart(2, '0');
|
||||
}
|
||||
update();
|
||||
setInterval(update, 1000);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
54
templates/trash.html
Normal file
54
templates/trash.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Trash<span class="page-count">{{ total_deleted }}</span></h1>
|
||||
{% if total_deleted > 0 %}
|
||||
<form action="/admin/trash/empty" method="post" data-confirm="Permanently delete ALL {{ total_deleted }} items? This cannot be undone." style="display:inline">
|
||||
<button type="submit" class="btn btn-danger">Empty Trash</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Type filter -->
|
||||
<div class="filters-bar">
|
||||
<a href="/admin/trash" class="btn {{ 'btn-primary' if not current_type else 'btn-secondary' }} btn-sm">
|
||||
All ({{ total_deleted }})
|
||||
</a>
|
||||
{% for entity in trash_entities %}
|
||||
{% set count = entity_counts.get(entity.table, 0) %}
|
||||
{% if count > 0 %}
|
||||
<a href="/admin/trash?entity_type={{ entity.table }}" class="btn {{ 'btn-primary' if current_type == entity.table else 'btn-secondary' }} btn-sm">
|
||||
{{ entity.label }} ({{ count }})
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if deleted_items %}
|
||||
<div class="card mt-3">
|
||||
{% for item in deleted_items %}
|
||||
<div class="list-row">
|
||||
<span class="search-type-badge search-type-{{ item.table }}">{{ item.type_label }}</span>
|
||||
<span class="row-title">{{ item.name }}</span>
|
||||
{% if item.deleted_at %}
|
||||
<span class="row-meta">Deleted {{ item.deleted_at.strftime('%Y-%m-%d %H:%M') if item.deleted_at else '' }}</span>
|
||||
{% endif %}
|
||||
<div class="row-actions" style="opacity: 1;">
|
||||
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/restore" method="post" style="display:inline">
|
||||
<button type="submit" class="btn btn-secondary btn-xs">Restore</button>
|
||||
</form>
|
||||
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/permanent-delete" method="post"
|
||||
data-confirm="Permanently delete '{{ item.name }}'? This cannot be undone." style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Delete Forever</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mt-3">
|
||||
<div class="empty-state-icon">🗑</div>
|
||||
<div class="empty-state-text">Trash is empty</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
32
templates/weblink_folder_form.html
Normal file
32
templates/weblink_folder_form.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/weblinks/folders/create">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Folder Name *</label>
|
||||
<input type="text" name="name" class="form-input" required placeholder="Folder name...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Parent Folder</label>
|
||||
<select name="parent_id" class="form-select">
|
||||
<option value="">None (top-level)</option>
|
||||
{% for f in parent_folders %}
|
||||
<option value="{{ f.id }}">{{ f.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Create Folder</button>
|
||||
<a href="/weblinks" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
templates/weblink_form.html
Normal file
52
templates/weblink_form.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ page_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ '/weblinks/' ~ item.id ~ '/edit' if item else '/weblinks/create' }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Label *</label>
|
||||
<input type="text" name="label" class="form-input" required
|
||||
value="{{ item.label if item else '' }}" placeholder="Display name...">
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">URL *</label>
|
||||
<input type="url" name="url" class="form-input" required
|
||||
value="{{ item.url if item else '' }}" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Folder</label>
|
||||
<select name="folder_id" class="form-select">
|
||||
<option value="">None</option>
|
||||
{% for f in folders %}
|
||||
<option value="{{ f.id }}" {{ 'selected' if prefill_folder_id == f.id|string }}>{{ f.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
|
||||
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="2">{{ item.description if item and item.description else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
|
||||
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Add Link' }}</button>
|
||||
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=links' if prefill_task_id is defined and prefill_task_id else '/weblinks' }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
105
templates/weblinks.html
Normal file
105
templates/weblinks.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Bookmarks<span class="page-count">{{ items|length }}</span></h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/weblinks/folders/create" class="btn btn-secondary">+ New Folder</a>
|
||||
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">+ New Link</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="weblinks-layout">
|
||||
<!-- Folder sidebar -->
|
||||
<div class="weblinks-folders">
|
||||
<a href="/weblinks" class="weblink-folder-item {{ 'active' if not current_folder_id }}">
|
||||
All Links
|
||||
</a>
|
||||
{% for folder in top_folders %}
|
||||
<a href="/weblinks?folder_id={{ folder.id }}" class="weblink-folder-item {{ 'active' if current_folder_id == folder.id|string }}">
|
||||
{{ folder.name }}
|
||||
{% if folder.link_count %}<span class="badge" style="margin-left: auto;">{{ folder.link_count }}</span>{% endif %}
|
||||
</a>
|
||||
{% for child in child_folder_map.get(folder.id|string, []) %}
|
||||
<a href="/weblinks?folder_id={{ child.id }}" class="weblink-folder-item {{ 'active' if current_folder_id == child.id|string }}" style="padding-left: 28px;">
|
||||
{{ child.name }}
|
||||
{% if child.link_count %}<span class="badge" style="margin-left: auto;">{{ child.link_count }}</span>{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Links list -->
|
||||
<div class="weblinks-content">
|
||||
{% if current_folder %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 style="font-size: 1rem; font-weight: 600;">{{ current_folder.name }}</h2>
|
||||
<form action="/weblinks/folders/{{ current_folder.id }}/delete" method="post"
|
||||
data-confirm="Delete folder '{{ current_folder.name }}'?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Delete Folder</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if available_links %}
|
||||
<div class="card" style="margin-bottom: 12px;">
|
||||
<form action="/weblinks/folders/{{ current_folder.id }}/add-link" method="post"
|
||||
style="display: flex; gap: 8px; align-items: end; padding: 12px;">
|
||||
<div class="form-group" style="flex: 1; margin: 0;">
|
||||
<label class="form-label">Add existing link</label>
|
||||
<select name="link_id" class="form-select" required>
|
||||
<option value="">Select link...</option>
|
||||
{% for l in available_links %}
|
||||
<option value="{{ l.id }}">{{ l.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
{% for item in items %}
|
||||
<div class="list-row">
|
||||
<span class="row-title">
|
||||
<a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.label }}</a>
|
||||
</span>
|
||||
<span class="row-meta text-xs" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.url }}
|
||||
</span>
|
||||
{% if item.tags %}
|
||||
{% for tag in item.tags %}
|
||||
<span class="row-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="row-actions">
|
||||
{% if current_folder_id %}
|
||||
<form action="/weblinks/folders/{{ current_folder_id }}/reorder" method="post" style="display:inline">
|
||||
<input type="hidden" name="link_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="direction" value="up">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" title="Move up">▲</button>
|
||||
</form>
|
||||
<form action="/weblinks/folders/{{ current_folder_id }}/reorder" method="post" style="display:inline">
|
||||
<input type="hidden" name="link_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="direction" value="down">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" title="Move down">▼</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="/weblinks/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<form action="/weblinks/{{ item.id }}/delete" method="post" data-confirm="Delete this link?" style="display:inline">
|
||||
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔗</div>
|
||||
<div class="empty-state-text">No links{{ ' in this folder' if current_folder }} yet</div>
|
||||
<a href="/weblinks/create{{ '?folder_id=' ~ current_folder_id if current_folder_id }}" class="btn btn-primary">Add Link</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user