Web UI - Infor CloudSuite-style split-panel layout (items.html rewrite): - Replace modal-based item detail with inline split-panel workspace - Horizontal mode: item list on left, tabbed detail panel on right - Vertical mode: detail panel on top, item list below - Detail tabs: Main, Properties, Revisions, BOM, Where Used - Ctrl+F opens in-page filter overlay with fuzzy search - Column config gear icon with per-layout-mode persistence - Search scope toggle pills (All / Part Number / Description) - Selected row highlight with accent border - Responsive breakpoint forces vertical below 900px - Create/Edit/Delete remain as modal dialogs Web UI - Projects page: - New projects.html template with full CRUD - Project table: Code, Name, Description, Item count, Created, Actions - Create/Edit/Delete modals - Click project code navigates to items filtered by project - 3-tab navigation in base.html: Items, Projects, Schemas Fuzzy search: - Add sahilm/fuzzy dependency for ranked text matching - New internal/api/search.go with SearchableItems fuzzy.Source - GET /api/items/search endpoint with field scope and type/project filters - Frontend routes to fuzzy endpoint when search input is non-empty Odoo ERP integration scaffold: - Migration 008: integrations and sync_log tables - internal/odoo/ package: types, client stubs, sync stubs - internal/db/integrations.go: IntegrationRepository - internal/config/config.go: OdooConfig struct - 6 API endpoints for config CRUD, sync log, test, push, pull - All sync operations return stub responses Documentation: - docs/REPOSITORY_STATUS.md: comprehensive repository state report with architecture overview, API surface, feature stubs, and potential issues analysis
301 lines
12 KiB
HTML
301 lines
12 KiB
HTML
{{define "projects_content"}}
|
|
<div class="stats-grid" id="project-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="project-count">-</div>
|
|
<div class="stat-label">Total Projects</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Projects</h2>
|
|
<button class="btn btn-primary" onclick="openCreateProjectModal()">+ New Project</button>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Code</th>
|
|
<th>Name</th>
|
|
<th>Description</th>
|
|
<th>Items</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="projects-table">
|
|
<tr>
|
|
<td colspan="6">
|
|
<div class="loading"><div class="spinner"></div></div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Project Modal -->
|
|
<div class="modal-overlay" id="create-project-modal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Create New Project</h3>
|
|
<button class="modal-close" onclick="closeCreateProjectModal()">×</button>
|
|
</div>
|
|
<form id="create-project-form" onsubmit="createProject(event)">
|
|
<div class="form-group">
|
|
<label class="form-label">Code (2-10 characters, uppercase)</label>
|
|
<input type="text" class="form-input" id="project-code" required
|
|
minlength="2" maxlength="10" pattern="[A-Za-z0-9\-]+"
|
|
placeholder="e.g., PROJ-A" style="text-transform: uppercase;" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-input" id="project-name" placeholder="Project name" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<input type="text" class="form-input" id="project-description" placeholder="Project description" />
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="closeCreateProjectModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create Project</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Project Modal -->
|
|
<div class="modal-overlay" id="edit-project-modal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Edit Project</h3>
|
|
<button class="modal-close" onclick="closeEditProjectModal()">×</button>
|
|
</div>
|
|
<form id="edit-project-form" onsubmit="saveProject(event)">
|
|
<input type="hidden" id="edit-project-code" />
|
|
<div class="form-group">
|
|
<label class="form-label">Code</label>
|
|
<input type="text" class="form-input" id="edit-project-code-display" disabled />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-input" id="edit-project-name" placeholder="Project name" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<input type="text" class="form-input" id="edit-project-description" placeholder="Project description" />
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="closeEditProjectModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Project Modal -->
|
|
<div class="modal-overlay" id="delete-project-modal">
|
|
<div class="modal" style="max-width: 400px">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Delete Project</h3>
|
|
<button class="modal-close" onclick="closeDeleteProjectModal()">×</button>
|
|
</div>
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<p>Are you sure you want to permanently delete project <strong id="delete-project-code"></strong>?</p>
|
|
<p style="color: var(--ctp-red); margin-top: 0.5rem;">This action cannot be undone.</p>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="closeDeleteProjectModal()">Cancel</button>
|
|
<button type="button" class="btn btn-primary" style="background-color: var(--ctp-red)" onclick="confirmDeleteProject()">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}} {{define "projects_scripts"}}
|
|
<script>
|
|
let projectToDelete = null;
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
async function loadProjects() {
|
|
const tbody = document.getElementById('projects-table');
|
|
|
|
try {
|
|
const response = await fetch('/api/projects');
|
|
const projects = await response.json();
|
|
|
|
document.getElementById('project-count').textContent = projects.length;
|
|
|
|
if (!projects || projects.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><h3>No projects found</h3><p>Create your first project to start organizing items.</p></div></td></tr>';
|
|
return;
|
|
}
|
|
|
|
// Fetch item counts for each project
|
|
const projectsWithCounts = await Promise.all(projects.map(async (project) => {
|
|
try {
|
|
const itemsRes = await fetch(`/api/projects/${project.code}/items`);
|
|
const items = await itemsRes.json();
|
|
return { ...project, itemCount: Array.isArray(items) ? items.length : 0 };
|
|
} catch {
|
|
return { ...project, itemCount: 0 };
|
|
}
|
|
}));
|
|
|
|
tbody.innerHTML = projectsWithCounts.map(project => `
|
|
<tr>
|
|
<td><a href="/?project=${encodeURIComponent(project.code)}" style="color: var(--ctp-peach); font-family: 'JetBrains Mono', monospace; font-weight: 500;">${project.code}</a></td>
|
|
<td>${project.name || '-'}</td>
|
|
<td>${project.description || '-'}</td>
|
|
<td>${project.itemCount}</td>
|
|
<td>${formatDate(project.created_at)}</td>
|
|
<td>
|
|
<button class="btn btn-secondary" style="padding: 0.4rem 0.75rem; font-size: 0.85rem;" onclick="openEditProjectModal('${project.code}')">Edit</button>
|
|
<button class="btn btn-secondary" style="padding: 0.4rem 0.75rem; font-size: 0.85rem; background-color: var(--ctp-surface2);" onclick="openDeleteProjectModal('${project.code}')">Delete</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (error) {
|
|
tbody.innerHTML = `<tr><td colspan="6"><div class="empty-state"><h3>Error loading projects</h3><p>${error.message}</p></div></td></tr>`;
|
|
}
|
|
}
|
|
|
|
// Create
|
|
function openCreateProjectModal() {
|
|
document.getElementById('create-project-modal').classList.add('active');
|
|
}
|
|
|
|
function closeCreateProjectModal() {
|
|
document.getElementById('create-project-modal').classList.remove('active');
|
|
document.getElementById('create-project-form').reset();
|
|
}
|
|
|
|
async function createProject(event) {
|
|
event.preventDefault();
|
|
|
|
const data = {
|
|
code: document.getElementById('project-code').value.toUpperCase(),
|
|
name: document.getElementById('project-name').value,
|
|
description: document.getElementById('project-description').value,
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/projects', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
alert(`Error: ${error.message || error.error}`);
|
|
return;
|
|
}
|
|
|
|
closeCreateProjectModal();
|
|
loadProjects();
|
|
} catch (error) {
|
|
alert(`Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Edit
|
|
async function openEditProjectModal(code) {
|
|
document.getElementById('edit-project-modal').classList.add('active');
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${code}`);
|
|
const project = await response.json();
|
|
|
|
document.getElementById('edit-project-code').value = code;
|
|
document.getElementById('edit-project-code-display').value = code;
|
|
document.getElementById('edit-project-name').value = project.name || '';
|
|
document.getElementById('edit-project-description').value = project.description || '';
|
|
} catch (error) {
|
|
alert(`Error loading project: ${error.message}`);
|
|
closeEditProjectModal();
|
|
}
|
|
}
|
|
|
|
function closeEditProjectModal() {
|
|
document.getElementById('edit-project-modal').classList.remove('active');
|
|
document.getElementById('edit-project-form').reset();
|
|
}
|
|
|
|
async function saveProject(event) {
|
|
event.preventDefault();
|
|
|
|
const code = document.getElementById('edit-project-code').value;
|
|
const data = {
|
|
name: document.getElementById('edit-project-name').value,
|
|
description: document.getElementById('edit-project-description').value,
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${code}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
alert(`Error: ${error.message || error.error}`);
|
|
return;
|
|
}
|
|
|
|
closeEditProjectModal();
|
|
loadProjects();
|
|
} catch (error) {
|
|
alert(`Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Delete
|
|
function openDeleteProjectModal(code) {
|
|
projectToDelete = code;
|
|
document.getElementById('delete-project-code').textContent = code;
|
|
document.getElementById('delete-project-modal').classList.add('active');
|
|
}
|
|
|
|
function closeDeleteProjectModal() {
|
|
document.getElementById('delete-project-modal').classList.remove('active');
|
|
projectToDelete = null;
|
|
}
|
|
|
|
async function confirmDeleteProject() {
|
|
if (!projectToDelete) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectToDelete}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!response.ok && response.status !== 204) {
|
|
const error = await response.json();
|
|
alert(`Error: ${error.message || error.error}`);
|
|
return;
|
|
}
|
|
|
|
closeDeleteProjectModal();
|
|
loadProjects();
|
|
} catch (error) {
|
|
alert(`Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Close modals on overlay click
|
|
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) overlay.classList.remove('active');
|
|
});
|
|
});
|
|
|
|
loadProjects();
|
|
</script>
|
|
{{end}}
|