Compare commits
3 Commits
eba273be49
...
92e7ea4667
| Author | SHA1 | Date | |
|---|---|---|---|
|
92e7ea4667
|
|||
|
b93d9382ac
|
|||
|
a31c516e8f
|
@@ -9,10 +9,11 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string `yaml:"port"`
|
||||
DatabasePath string `yaml:"database_path"`
|
||||
CertificatePath string `yaml:"certificate_path"`
|
||||
PrivateKeyPath string `yaml:"private_key_path"`
|
||||
Port string `yaml:"port"`
|
||||
DatabasePath string `yaml:"database_path"`
|
||||
CertificatePath string `yaml:"certificate_path"`
|
||||
PrivateKeyPath string `yaml:"private_key_path"`
|
||||
AllowRegistration bool `yaml:"allow_registration"`
|
||||
}
|
||||
|
||||
const configPath = "./appdata/config.yaml"
|
||||
@@ -34,10 +35,11 @@ func CheckIfExists() error {
|
||||
}
|
||||
|
||||
defaultConfig := Config{
|
||||
Port: "8080",
|
||||
DatabasePath: "./appdata/database.db",
|
||||
CertificatePath: "./appdata/cert.pem",
|
||||
PrivateKeyPath: "./appdata/key.pem",
|
||||
Port: "8080",
|
||||
DatabasePath: "./appdata/database.db",
|
||||
CertificatePath: "./appdata/cert.pem",
|
||||
PrivateKeyPath: "./appdata/key.pem",
|
||||
AllowRegistration: true,
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(defaultConfig)
|
||||
|
||||
@@ -96,7 +96,7 @@ function editItem(item) {
|
||||
document.getElementById('item-name').value = item.name;
|
||||
document.getElementById('item-category').value = item.category;
|
||||
document.getElementById('item-desc').value = item.description;
|
||||
document.getElementById('item-qty').value = item.total_quantity;
|
||||
//document.getElementById('item-qty').value = item.total_quantity;
|
||||
document.getElementById('item-modal-title').innerText = 'Edit Item';
|
||||
document.getElementById('item-modal').classList.add('show');
|
||||
}
|
||||
@@ -108,7 +108,7 @@ async function saveItem(event) {
|
||||
name: document.getElementById('item-name').value,
|
||||
category: document.getElementById('item-category').value,
|
||||
description: document.getElementById('item-desc').value,
|
||||
total_quantity: parseInt(document.getElementById('item-qty').value, 10)
|
||||
//total_quantity: parseInt(document.getElementById('item-qty').value, 10)
|
||||
};
|
||||
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
@@ -385,3 +385,71 @@ async function deleteAssociation(assocId, projectId) {
|
||||
await reloadAssociationTable(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDashboardView() {
|
||||
const locTbody = document.getElementById('dash-locations-body');
|
||||
const projTbody = document.getElementById('dash-projects-body');
|
||||
if (!locTbody && !projTbody) return;
|
||||
|
||||
try {
|
||||
const locData = await apiRequest('/api/location');
|
||||
if (locTbody && locData && locData.locations) {
|
||||
locTbody.innerHTML = '';
|
||||
if (locData.locations.length === 0) {
|
||||
locTbody.innerHTML = '<tr><td style="color: var(--text-muted); padding: 1rem;">No locations found.</td></tr>';
|
||||
} else {
|
||||
locData.locations.forEach(loc => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td style="cursor: pointer; color: var(--accent); font-weight: 500; padding: 0.75rem 1rem;">${loc.name}</td>`;
|
||||
tr.onclick = () => openDashboardModal(`/api/location?id=${loc.id}&content=true`, `Items in ${loc.name}`, 'contents');
|
||||
locTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const projData = await apiRequest('/api/project');
|
||||
if (projTbody && projData && projData.projects) {
|
||||
projTbody.innerHTML = '';
|
||||
if (projData.projects.length === 0) {
|
||||
projTbody.innerHTML = '<tr><td style="color: var(--text-muted); padding: 1rem;">No projects found.</td></tr>';
|
||||
} else {
|
||||
projData.projects.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td style="cursor: pointer; color: var(--accent); font-weight: 500; padding: 0.75rem 1rem;">${p.name}</td>`;
|
||||
tr.onclick = () => openDashboardModal(`/api/project?id=${p.id}&details=true`, `Items assigned to ${p.name}`, 'items');
|
||||
projTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDashboardModal(url, title, dataKey) {
|
||||
document.getElementById('dash-modal-title').innerText = title;
|
||||
const tbody = document.getElementById('dash-modal-body');
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '<tr><td colspan="2" style="text-align:center; padding:1.5rem; color:var(--text-muted);">Loading...</td></tr>';
|
||||
document.getElementById('dash-details-modal').classList.add('show');
|
||||
|
||||
try {
|
||||
const data = await apiRequest(url);
|
||||
tbody.innerHTML = '';
|
||||
const list = data[dataKey] || [];
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="2" style="color:var(--text-muted); text-align:center; padding:1.5rem;">No active items found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
list.forEach(i => {
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td style="color:var(--text); padding:0.75rem 1rem;">${i.item_name}</td>
|
||||
<td style="text-align:right; padding:0.75rem 1rem;"><span class="badge success">${i.quantity}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="2" style="color:var(--error); text-align:center; padding:1.5rem;">Failed to load data.</td></tr>';
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func Home(w http.ResponseWriter, r *http.Request) {
|
||||
err := home.Execute(w, struct {
|
||||
Name string
|
||||
}{
|
||||
Name: "Miau",
|
||||
Name: "Home",
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -50,8 +50,22 @@ func Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
err := dashboard.ExecuteTemplate(w, "base.html", struct {
|
||||
Title string
|
||||
Stats struct {
|
||||
Items int
|
||||
Projects int
|
||||
Locations int
|
||||
}
|
||||
}{
|
||||
Title: "Miau",
|
||||
Title: "Dashboard",
|
||||
Stats: struct {
|
||||
Items int
|
||||
Projects int
|
||||
Locations int
|
||||
}{
|
||||
Items: 1,
|
||||
Projects: 1,
|
||||
Locations: 3,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -62,7 +76,7 @@ func Inventory(w http.ResponseWriter, r *http.Request) {
|
||||
err := inventory.ExecuteTemplate(w, "base.html", struct {
|
||||
Title string
|
||||
}{
|
||||
Title: "Miau",
|
||||
Title: "Inventory",
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -73,7 +87,7 @@ func Items(w http.ResponseWriter, r *http.Request) {
|
||||
err := item.ExecuteTemplate(w, "base.html", struct {
|
||||
Title string
|
||||
}{
|
||||
Title: "Miau",
|
||||
Title: "Items",
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -84,7 +98,7 @@ func Locations(w http.ResponseWriter, r *http.Request) {
|
||||
err := locations.ExecuteTemplate(w, "base.html", struct {
|
||||
Title string
|
||||
}{
|
||||
Title: "Miau",
|
||||
Title: "Locations",
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -95,7 +109,7 @@ func Projects(w http.ResponseWriter, r *http.Request) {
|
||||
err := projects.ExecuteTemplate(w, "base.html", struct {
|
||||
Title string
|
||||
}{
|
||||
Title: "Miau",
|
||||
Title: "Projects",
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Title }} | MiauInv</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script src="/assets/js/api.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/theme.css">
|
||||
<link rel="stylesheet" href="/assets/css/dashboard.css">
|
||||
</head>
|
||||
|
||||
@@ -3,35 +3,108 @@
|
||||
<h1>Dashboard Overview</h1>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(59, 130, 246, 0.1); color: var(--accent);">
|
||||
<div class="stats-grid" style="margin-bottom: 2rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
|
||||
<div class="stat-card" style="background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="stat-icon" style="background: rgba(59, 130, 246, 0.1); color: var(--accent); padding: 0.75rem; border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<p>Total Items</p>
|
||||
<h2>{{ .Stats.Items }}</h2>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem; margin: 0;">Total Items</p>
|
||||
<h2 style="font-size: 1.5rem; font-weight: 700; margin: 0;">
|
||||
{{ if .Stats }}{{ .Stats.Items }}{{ else }}0{{ end }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(16, 185, 129, 0.1); color: var(--success);">
|
||||
<div class="stat-card" style="background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="stat-icon" style="background: rgba(16, 185, 129, 0.1); color: var(--success); padding: 0.75rem; border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"></path></svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<p>Active Projects</p>
|
||||
<h2>{{ .Stats.Projects }}</h2>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem; margin: 0;">Active Projects</p>
|
||||
<h2 style="font-size: 1.5rem; font-weight: 700; margin: 0;">
|
||||
{{ if .Stats }}{{ .Stats.Projects }}{{ else }}0{{ end }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: rgba(245, 158, 11, 0.1); color: #f59e0b;">
|
||||
<div class="stat-card" style="background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="stat-icon" style="background: rgba(245, 158, 11, 0.1); color: #f59e0b; padding: 0.75rem; border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<p>Locations</p>
|
||||
<h2>{{ .Stats.Locations }}</h2>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem; margin: 0;">Locations</p>
|
||||
<h2 style="font-size: 1.5rem; font-weight: 700; margin: 0;">
|
||||
{{ if .Stats }}{{ .Stats.Locations }}{{ else }}0{{ end }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-split">
|
||||
<div class="card" style="max-width: 100%; text-align: left; padding: 1.5rem;">
|
||||
<h2 style="font-size: 1.25rem; margin-bottom: 1rem; color: var(--text);">Locations</h2>
|
||||
<div class="table-container">
|
||||
<table class="inner-table" style="margin-top: 0; border-radius: 8px; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: left; padding: 0.75rem 1rem; background: #111827;">Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dash-locations-body">
|
||||
<tr><td class="table-loader" style="padding: 1.5rem; text-align: center; color: var(--text-muted);">Loading locations...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 100%; text-align: left; padding: 1.5rem;">
|
||||
<h2 style="font-size: 1.25rem; margin-bottom: 1rem; color: var(--text);">Active Projects</h2>
|
||||
<div class="table-container">
|
||||
<table class="inner-table" style="margin-top: 0; border-radius: 8px; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: left; padding: 0.75rem 1rem; background: #111827;">Project Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dash-projects-body">
|
||||
<tr><td class="table-loader" style="padding: 1.5rem; text-align: center; color: var(--text-muted);">Loading projects...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dash-details-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 id="dash-modal-title">Details</h2>
|
||||
<div class="table-container">
|
||||
<table class="inner-table" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: left; padding: 0.75rem 1rem;">Item</th>
|
||||
<th style="text-align: right; width: 100px; padding: 0.75rem 1rem;">Quantity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dash-modal-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('dash-details-modal').classList.remove('show')">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (typeof handleDashboardView === "function") {
|
||||
handleDashboardView();
|
||||
}
|
||||
if (window.htmx) {
|
||||
htmx.on("htmx:afterOnLoad", function() {
|
||||
if (typeof handleDashboardView === "function") {
|
||||
handleDashboardView();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
134
frontend/htmx/register_blocked.html
Normal file
134
frontend/htmx/register_blocked.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registration Disabled - MiauInv</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #111827;
|
||||
--card: #1f2937;
|
||||
--border: #374151;
|
||||
--text: #f9fafb;
|
||||
--text-muted: #9ca3af;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body:not(.dashboard-layout) {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.card .subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.85rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 1.25rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
color: var(--error);
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
<h1>Registration</h1>
|
||||
<div class="subtitle">Create a new account</div>
|
||||
|
||||
<div class="message error">
|
||||
<strong>Access Denied:</strong> Public registration is currently disabled for this system. Please contact your system administrator to request an account.
|
||||
</div>
|
||||
|
||||
<div class="footer-text">
|
||||
<a class="btn btn-secondary" href="/login" style="margin-top: 1.5rem;">Back to Login</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
106
handlers/api.go
106
handlers/api.go
@@ -18,6 +18,32 @@ func Location(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
case http.MethodGet:
|
||||
idStr := r.URL.Query().Get("id")
|
||||
contentMode := r.URL.Query().Get("content")
|
||||
|
||||
if idStr != "" && contentMode == "true" {
|
||||
_, _ = strconv.Atoi(idStr)
|
||||
query := `
|
||||
SELECT s.item_id, i.name, s.quantity
|
||||
FROM stock s
|
||||
JOIN items i ON s.item_id = i.id
|
||||
WHERE s.location_id = ? AND s.quantity > 0
|
||||
`
|
||||
rows, err := storage.DB.Query(query)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var contents []models.LocationContent
|
||||
for rows.Next() {
|
||||
var c models.LocationContent
|
||||
rows.Scan(&c.ItemID, &c.ItemName, &c.Quantity)
|
||||
contents = append(contents, c)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"contents": contents})
|
||||
return
|
||||
}
|
||||
|
||||
if idStr != "" {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
@@ -188,46 +214,33 @@ func Item(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
idStr := r.URL.Query().Get("id")
|
||||
|
||||
// Read One
|
||||
if idStr != "" {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid ID parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var item models.Item
|
||||
err = storage.DB.QueryRow("SELECT id, name, category, description, total_quantity FROM items WHERE id = ?", id).
|
||||
Scan(&item.ID, &item.Name, &item.Category, &item.Description, &item.TotalQuantity)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Item not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(item)
|
||||
return
|
||||
}
|
||||
|
||||
// Read All
|
||||
rows, err := storage.DB.Query("SELECT id, name, category, description, total_quantity FROM items ORDER BY name ASC")
|
||||
query := `
|
||||
SELECT
|
||||
i.id, i.name, i.category, i.description,
|
||||
COALESCE((SELECT SUM(quantity) FROM stock WHERE item_id = i.id), 0) as total_qty,
|
||||
COALESCE((SELECT SUM(quantity) FROM project_items WHERE item_id = i.id), 0) as allocated_qty
|
||||
FROM items i
|
||||
ORDER BY i.name ASC
|
||||
`
|
||||
rows, err := storage.DB.Query(query)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []models.Item{}
|
||||
var itemsExtended []models.ItemExtended
|
||||
for rows.Next() {
|
||||
var item models.Item
|
||||
rows.Scan(&item.ID, &item.Name, &item.Category, &item.Description, &item.TotalQuantity)
|
||||
items = append(items, item)
|
||||
var ie models.ItemExtended
|
||||
err := rows.Scan(&ie.ID, &ie.Name, &ie.Category, &ie.Description, &ie.TotalQuantity, &ie.AllocatedQty)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ie.AvailableQty = ie.TotalQuantity - ie.AllocatedQty
|
||||
itemsExtended = append(itemsExtended, ie)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"items": items})
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"items": itemsExtended})
|
||||
return
|
||||
|
||||
case http.MethodPost:
|
||||
var body models.Item
|
||||
@@ -304,6 +317,33 @@ func Project(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodGet:
|
||||
idStr := r.URL.Query().Get("id")
|
||||
|
||||
detailsMode := r.URL.Query().Get("details")
|
||||
|
||||
if idStr != "" && detailsMode == "true" {
|
||||
_, _ = strconv.Atoi(idStr)
|
||||
query := `
|
||||
SELECT pi.item_id, i.name, pi.quantity
|
||||
FROM project_items pi
|
||||
JOIN items i ON pi.item_id = i.id
|
||||
WHERE pi.project_id = ?
|
||||
`
|
||||
rows, err := storage.DB.Query(query)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var details []models.ProjectDetailItem
|
||||
for rows.Next() {
|
||||
var d models.ProjectDetailItem
|
||||
rows.Scan(&d.ItemID, &d.ItemName, &d.Quantity)
|
||||
details = append(details, d)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"items": details})
|
||||
return
|
||||
}
|
||||
|
||||
if idStr != "" {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
var p models.Project
|
||||
|
||||
@@ -42,3 +42,25 @@ type ProjectItem struct {
|
||||
ProjectID int `json:"project_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
type ItemExtended struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
TotalQuantity int `json:"total_quantity"` // Summe aus allen Stock-Einträgen
|
||||
AllocatedQty int `json:"allocated_quantity"` // Summe aus allen Projekt-Zuweisungen
|
||||
AvailableQty int `json:"available_quantity"` // Total - Allocated
|
||||
}
|
||||
|
||||
type LocationContent struct {
|
||||
ItemID int `json:"item_id"`
|
||||
ItemName string `json:"item_name"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
type ProjectDetailItem struct {
|
||||
ItemID int `json:"item_id"`
|
||||
ItemName string `json:"item_name"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Port string
|
||||
JWTSecret []byte
|
||||
DatabasePath string
|
||||
CertificatePath string
|
||||
PrivateKeyPath string
|
||||
Port string
|
||||
JWTSecret []byte
|
||||
DatabasePath string
|
||||
CertificatePath string
|
||||
PrivateKeyPath string
|
||||
AllowRegistration bool
|
||||
}
|
||||
|
||||
func InitServer() *Server {
|
||||
@@ -44,11 +45,12 @@ func InitServer() *Server {
|
||||
}
|
||||
|
||||
return &Server{
|
||||
Port: cfg.Port,
|
||||
JWTSecret: []byte(jwtSecret),
|
||||
DatabasePath: cfg.DatabasePath,
|
||||
CertificatePath: cfg.CertificatePath,
|
||||
PrivateKeyPath: cfg.PrivateKeyPath,
|
||||
Port: cfg.Port,
|
||||
JWTSecret: []byte(jwtSecret),
|
||||
DatabasePath: cfg.DatabasePath,
|
||||
CertificatePath: cfg.CertificatePath,
|
||||
PrivateKeyPath: cfg.PrivateKeyPath,
|
||||
AllowRegistration: cfg.AllowRegistration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,21 +63,26 @@ func (this *Server) Run() {
|
||||
//
|
||||
mux.HandleFunc("/", frontend.Home)
|
||||
mux.HandleFunc("/login", utils.RenderFile("frontend/htmx/login.html"))
|
||||
mux.HandleFunc("/register", utils.RenderFile("frontend/htmx/register.html"))
|
||||
|
||||
mux.Handle("/dashboard", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Dashboard)))
|
||||
mux.Handle("/inventory", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Inventory)))
|
||||
mux.Handle("/items", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Items)))
|
||||
mux.Handle("/locations", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Locations)))
|
||||
mux.Handle("/projects", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Projects)))
|
||||
if this.AllowRegistration {
|
||||
mux.HandleFunc("/register", utils.RenderFile("frontend/htmx/register.html"))
|
||||
} else {
|
||||
mux.HandleFunc("/register", utils.RenderFile("frontend/htmx/register_blocked.html"))
|
||||
}
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
mux.HandleFunc("/api/login", handlers.APILogin)
|
||||
mux.HandleFunc("/api/register", handlers.APIRegister)
|
||||
mux.HandleFunc("/api/refresh", handlers.RefreshToken)
|
||||
mux.Handle("/api/logout", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Logout)))
|
||||
if this.AllowRegistration {
|
||||
mux.HandleFunc("/api/register", handlers.APIRegister)
|
||||
}
|
||||
|
||||
mux.Handle("/api/item", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Item)))
|
||||
mux.Handle("/api/location", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Location)))
|
||||
|
||||
Reference in New Issue
Block a user