botui/ui/suite/project/project.html

759 lines
20 KiB
HTML
Raw Normal View History

<!-- =============================================================================
PROJECT APP - Project Management with Gantt Chart
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="project-app">
<!-- Sidebar -->
<aside class="project-sidebar">
<div class="sidebar-header">
<h2 data-i18n="project-title">Projects</h2>
<button
class="btn-icon"
id="new-project-btn"
title="New Project"
hx-get="/api/ui/project/new"
hx-target="#project-modal"
hx-swap="innerHTML"
>
<span>+</span>
</button>
</div>
<div class="sidebar-search">
<input
type="search"
placeholder="Search projects..."
hx-get="/projects"
hx-trigger="keyup changed delay:300ms"
hx-target="#project-list"
hx-swap="innerHTML"
name="q"
/>
</div>
<nav class="sidebar-nav">
<div
id="project-list"
hx-get="/projects"
hx-trigger="load, projectCreated from:body, projectDeleted from:body"
hx-swap="innerHTML"
>
<div class="loading-placeholder">Loading projects...</div>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="project-main">
<!-- Project Header -->
<header class="project-header">
<div class="project-info">
<h1 id="project-name">Select a Project</h1>
<div class="project-meta">
<span class="meta-item" id="project-status">
<span class="status-dot"></span>
<span>No project selected</span>
</span>
<span class="meta-item" id="project-progress">
<span class="progress-label">Progress:</span>
<span class="progress-value">--</span>
</span>
</div>
</div>
<div class="project-actions">
<div class="view-toggle">
<button class="view-btn active" data-view="gantt" onclick="switchView('gantt')">
<span>📊</span> Gantt
</button>
<button class="view-btn" data-view="timeline" onclick="switchView('timeline')">
<span>📅</span> Timeline
</button>
<button class="view-btn" data-view="list" onclick="switchView('list')">
<span>📋</span> List
</button>
<button class="view-btn" data-view="board" onclick="switchView('board')">
<span>📌</span> Board
</button>
</div>
<button
class="btn-primary"
id="add-task-btn"
hx-get="/api/ui/project/task/new"
hx-target="#project-modal"
hx-swap="innerHTML"
disabled
>
<span>+</span> Add Task
</button>
</div>
</header>
<!-- View Containers -->
<div class="project-views">
<!-- Gantt Chart View -->
<div id="gantt-view" class="view-container active">
<div class="gantt-toolbar">
<div class="gantt-zoom">
<button class="zoom-btn" onclick="zoomGantt('day')">Day</button>
<button class="zoom-btn active" onclick="zoomGantt('week')">Week</button>
<button class="zoom-btn" onclick="zoomGantt('month')">Month</button>
<button class="zoom-btn" onclick="zoomGantt('quarter')">Quarter</button>
</div>
<div class="gantt-filters">
<label>
<input type="checkbox" id="show-critical" checked onchange="toggleCriticalPath()">
<span data-i18n="project-critical-path">Show Critical Path</span>
</label>
<label>
<input type="checkbox" id="show-milestones" checked onchange="toggleMilestones()">
<span data-i18n="project-milestones">Show Milestones</span>
</label>
</div>
<button class="btn-secondary" onclick="fitGanttToScreen()">
Fit to Screen
</button>
</div>
<div class="gantt-container">
<div class="gantt-table">
<div class="gantt-table-header">
<div class="col-name">Task Name</div>
<div class="col-start">Start</div>
<div class="col-end">End</div>
<div class="col-duration">Duration</div>
<div class="col-progress">Progress</div>
<div class="col-assignee">Assignee</div>
</div>
<div
id="gantt-table-body"
class="gantt-table-body"
hx-get="/api/ui/project/tasks"
hx-trigger="projectSelected from:body"
hx-swap="innerHTML"
>
<div class="empty-state-inline">
Select a project to view tasks
</div>
</div>
</div>
<div class="gantt-chart">
<div class="gantt-timeline-header" id="gantt-timeline-header">
<!-- Timeline headers generated by JS -->
</div>
<div
id="gantt-chart-body"
class="gantt-chart-body"
hx-get="/api/ui/project/gantt"
hx-trigger="projectSelected from:body"
hx-swap="innerHTML"
>
<div class="empty-state-inline">
<p>No tasks to display</p>
</div>
</div>
</div>
</div>
</div>
<!-- Timeline View -->
<div id="timeline-view" class="view-container">
<div
class="timeline-container"
hx-get="/api/ui/project/timeline"
hx-trigger="projectSelected from:body"
hx-swap="innerHTML"
>
<div class="empty-state-inline">Select a project to view timeline</div>
</div>
</div>
<!-- List View -->
<div id="list-view" class="view-container">
<div
class="list-container"
hx-get="/api/ui/project/tasks/list"
hx-trigger="projectSelected from:body"
hx-swap="innerHTML"
>
<div class="empty-state-inline">Select a project to view tasks</div>
</div>
</div>
<!-- Board View -->
<div id="board-view" class="view-container">
<div class="board-columns">
<div class="board-column" data-status="not-started">
<h3>Not Started</h3>
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=not_started" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
</div>
<div class="board-column" data-status="in-progress">
<h3>In Progress</h3>
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=in_progress" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
</div>
<div class="board-column" data-status="completed">
<h3>Completed</h3>
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=completed" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div id="project-empty" class="empty-state">
<div class="empty-state-icon">📋</div>
<h2>No Project Selected</h2>
<p>Select a project from the sidebar or create a new one</p>
<button
class="btn-primary"
hx-get="/api/ui/project/new"
hx-target="#project-modal"
hx-swap="innerHTML"
>
<span>+</span> Create Project
</button>
</div>
</main>
<!-- Details Panel -->
<aside class="details-panel collapsed" id="details-panel">
<button class="panel-toggle" onclick="toggleDetailsPanel()">
<span></span>
</button>
<div class="panel-content">
<div id="task-details">
<p class="empty-message">Select a task to view details</p>
</div>
</div>
</aside>
<!-- Modal Container -->
<div id="project-modal" class="modal-container"></div>
</div>
<style>
.project-app {
display: flex;
height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
}
.project-sidebar {
width: 280px;
min-width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h2 {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
.sidebar-search {
padding: 0.75rem 1rem;
}
.sidebar-search input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.project-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.project-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.project-info h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
}
.project-meta {
display: flex;
gap: 1.5rem;
font-size: 0.875rem;
color: var(--text-muted);
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
margin-right: 0.375rem;
}
.project-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.view-toggle {
display: flex;
background: var(--bg-primary);
border-radius: 6px;
padding: 2px;
border: 1px solid var(--border-color);
}
.view-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 0.875rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.view-btn:hover {
color: var(--text-primary);
}
.view-btn.active {
background: var(--accent-color);
color: white;
}
.project-views {
flex: 1;
overflow: hidden;
position: relative;
}
.view-container {
display: none;
height: 100%;
overflow: auto;
}
.view-container.active {
display: flex;
flex-direction: column;
}
/* Gantt Chart Styles */
.gantt-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.gantt-zoom {
display: flex;
gap: 0.25rem;
}
.zoom-btn {
padding: 0.375rem 0.75rem;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
border-radius: 4px;
}
.zoom-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.gantt-filters {
display: flex;
gap: 1rem;
}
.gantt-filters label {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
}
.gantt-container {
flex: 1;
display: flex;
overflow: hidden;
}
.gantt-table {
width: 400px;
min-width: 400px;
border-right: 2px solid var(--border-color);
overflow-y: auto;
}
.gantt-table-header {
display: grid;
grid-template-columns: 1fr 80px 80px 70px 70px 90px;
padding: 0.75rem 0.5rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
position: sticky;
top: 0;
z-index: 10;
}
.gantt-table-body {
font-size: 0.875rem;
}
.gantt-chart {
flex: 1;
overflow: auto;
position: relative;
}
.gantt-timeline-header {
display: flex;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
min-width: max-content;
}
.gantt-chart-body {
position: relative;
min-height: 200px;
}
/* Board View */
.board-columns {
display: flex;
gap: 1rem;
padding: 1rem;
height: 100%;
overflow-x: auto;
}
.board-column {
width: 300px;
min-width: 300px;
background: var(--bg-secondary);
border-radius: 8px;
display: flex;
flex-direction: column;
}
.board-column h3 {
padding: 1rem;
margin: 0;
font-size: 0.875rem;
font-weight: 600;
border-bottom: 1px solid var(--border-color);
}
.column-tasks {
flex: 1;
padding: 0.5rem;
overflow-y: auto;
}
/* Details Panel */
.details-panel {
width: 320px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
transition: width 0.2s;
}
.details-panel.collapsed {
width: 48px;
}
.details-panel.collapsed .panel-content {
display: none;
}
.panel-toggle {
width: 100%;
padding: 0.75rem;
border: none;
background: transparent;
cursor: pointer;
font-size: 1.25rem;
}
.panel-content {
padding: 1rem;
overflow-y: auto;
height: calc(100% - 48px);
}
/* Empty State */
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: var(--text-muted);
}
.project-app:not(.has-project) .project-views {
display: none;
}
.project-app:not(.has-project) .empty-state {
display: flex;
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state-inline {
padding: 2rem;
text-align: center;
color: var(--text-muted);
}
/* Buttons */
.btn-icon {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--accent-color);
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
padding: 0.375rem 0.75rem;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
}
.loading-placeholder {
color: var(--text-muted);
font-size: 0.875rem;
padding: 1rem;
text-align: center;
}
.modal-container:empty {
display: none;
}
@media (max-width: 1024px) {
.gantt-table {
width: 300px;
min-width: 300px;
}
.gantt-table-header {
grid-template-columns: 1fr 70px 70px 60px;
}
.gantt-table-header .col-progress,
.gantt-table-header .col-assignee {
display: none;
}
}
@media (max-width: 768px) {
.project-sidebar {
position: absolute;
left: -280px;
height: 100%;
z-index: 50;
transition: left 0.2s;
}
.project-sidebar.open {
left: 0;
}
.details-panel {
display: none;
}
.view-toggle {
display: none;
}
}
</style>
<script>
let currentView = 'gantt';
let currentZoom = 'week';
function switchView(view) {
currentView = view;
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === view);
});
document.querySelectorAll('.view-container').forEach(container => {
container.classList.toggle('active', container.id === `${view}-view`);
});
}
function zoomGantt(level) {
currentZoom = level;
document.querySelectorAll('.zoom-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.toLowerCase() === level);
});
htmx.trigger('#gantt-chart-body', 'ganttZoomChanged');
}
function toggleCriticalPath() {
const show = document.getElementById('show-critical').checked;
document.querySelectorAll('.gantt-bar.critical').forEach(bar => {
bar.style.display = show ? '' : 'none';
});
}
function toggleMilestones() {
const show = document.getElementById('show-milestones').checked;
document.querySelectorAll('.gantt-milestone').forEach(ms => {
ms.style.display = show ? '' : 'none';
});
}
function fitGanttToScreen() {
const container = document.querySelector('.gantt-chart');
if (container) {
container.scrollLeft = 0;
}
}
function toggleDetailsPanel() {
const panel = document.getElementById('details-panel');
panel.classList.toggle('collapsed');
}
function selectProject(projectId) {
document.querySelector('.project-app').classList.add('has-project');
document.getElementById('add-task-btn').disabled = false;
htmx.trigger(document.body, 'projectSelected', { projectId: projectId });
}
document.addEventListener('DOMContentLoaded', function() {
generateTimelineHeaders();
});
function generateTimelineHeaders() {
const header = document.getElementById('gantt-timeline-header');
if (!header) return;
const today = new Date();
let html = '';
for (let i = 0; i < 30; i++) {
const date = new Date(today);
date.setDate(date.getDate() + i);
const day = date.getDate();
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
html += `
<div class="timeline-day ${isWeekend ? 'weekend' : ''}" style="width: 40px; text-align: center; padding: 0.5rem 0; border-right: 1px solid var(--border-color);">
<div style="font-size: 0.625rem; color: var(--text-muted);">${dayName}</div>
<div style="font-size: 0.75rem; font-weight: 600;">${day}</div>
</div>
`;
}
header.innerHTML = html;
}
</script>