Files
silo/internal/api/templates/projects.html
Forbes 7550b78740 feat: Infor-style split-panel layout, projects page, fuzzy search, Odoo scaffold
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
2026-01-31 09:20:27 -06:00

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()">&times;</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()">&times;</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()">&times;</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}}