mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 12:32:59 +00:00
test
This commit is contained in:
+323
-44
@@ -714,8 +714,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="appsList" class="space-y-4">
|
<div id="appsList" class="space-y-4">
|
||||||
<!-- Apps will be loaded here dynamically -->
|
<div class="mb-6">
|
||||||
<div class="text-center text-gray-500 py-4">Načítám seznam aplikací...</div>
|
<h4 class="font-medium text-gray-700 mb-3">Přednastavené aplikace</h4>
|
||||||
|
<div id="hardcodedAppsList" class="space-y-4">
|
||||||
|
<!-- Hardcoded apps will be loaded here -->
|
||||||
|
<div class="text-center text-gray-500 py-4">Načítám přednastavené aplikace...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 mb-4 border-t border-gray-200 pt-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h4 class="font-medium text-gray-700">Vlastní aplikace</h4>
|
||||||
|
<button id="addAppBtn" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus mr-2"></i>Přidat aplikaci
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="dynamicAppsList" class="space-y-4">
|
||||||
|
<!-- Dynamic apps will be loaded here -->
|
||||||
|
<div class="text-center text-gray-500 py-4">Načítám vlastní aplikace...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -749,8 +767,19 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="appIcon">Ikona (nepovinné)</label>
|
<label for="appIcon">Ikona (nepovinné)</label>
|
||||||
<input type="file" id="appIcon" class="form-control" accept="image/*">
|
<div class="mt-1 flex items-center">
|
||||||
<small class="text-gray-500 text-sm">Doporučená velikost: 64x64px</small>
|
<div class="relative">
|
||||||
|
<input type="file" id="appIcon" class="hidden" accept="image/*">
|
||||||
|
<label for="appIcon" class="cursor-pointer bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
|
<i class="fas fa-upload mr-2"></i>Vybrat soubor
|
||||||
|
</label>
|
||||||
|
<div id="fileName" class="text-sm text-gray-500 ml-2 truncate max-w-xs">Nebyl vybrán žádný soubor</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex items-center">
|
||||||
|
<img id="appIconPreview" src="" alt="Náhled ikony" class="h-12 w-12 rounded-full object-cover hidden">
|
||||||
|
<span class="text-xs text-gray-500 ml-2">Doporučená velikost: 64x64px (PNG, JPG, SVG)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-3 mt-6">
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
@@ -1360,44 +1389,121 @@ function handleImageUpload(event) {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// App Management Functions
|
// Hardcoded apps data - should match the ones in index.html
|
||||||
async function loadApps() {
|
const HARDCODED_APPS = [
|
||||||
|
{
|
||||||
|
id: 'hardcoded-car',
|
||||||
|
name: 'Záznam služebních jízd',
|
||||||
|
url: '/evidence-aut',
|
||||||
|
description: 'Jednoduchý systém pro evidenci a správu jízd služebními vozidly.',
|
||||||
|
icon: 'fa-car-side',
|
||||||
|
color: 'blue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded-lunch',
|
||||||
|
name: 'Objednávka obědů',
|
||||||
|
url: 'http://ppc-app/pwkweb2/',
|
||||||
|
description: 'Portál pro objednávku a přehled firemních obědů',
|
||||||
|
icon: 'fa-utensils',
|
||||||
|
color: 'green'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded-osticket',
|
||||||
|
name: 'OSTicket',
|
||||||
|
url: 'http://osticket/',
|
||||||
|
description: 'Systém technické podpory a hlášení problémů',
|
||||||
|
icon: 'fa-headset',
|
||||||
|
color: 'orange'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded-kanboard',
|
||||||
|
name: 'Kanboard',
|
||||||
|
url: 'http://kanboard/',
|
||||||
|
description: 'Správa úkolů a projektů v přehledném kanban stylu',
|
||||||
|
icon: 'fa-tasks',
|
||||||
|
color: 'purple'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load hardcoded apps
|
||||||
|
function loadHardcodedApps() {
|
||||||
|
const hardcodedAppsList = document.getElementById('hardcodedAppsList');
|
||||||
|
|
||||||
|
if (HARDCODED_APPS.length === 0) {
|
||||||
|
hardcodedAppsList.innerHTML = `
|
||||||
|
<div class="text-center py-4 text-gray-500">
|
||||||
|
Žádné přednastavené aplikace nebyly nalezeny
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hardcodedAppsList.innerHTML = HARDCODED_APPS.map(app => `
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-${app.color}-100 text-${app.color}-600 flex items-center justify-center">
|
||||||
|
<i class="fas ${app.icon} text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium">${app.name}</h4>
|
||||||
|
<p class="text-sm text-gray-500">${app.url}</p>
|
||||||
|
<p class="text-sm text-gray-400">${app.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||||
|
Přednastaveno
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dynamic apps
|
||||||
|
async function loadDynamicApps() {
|
||||||
|
const dynamicAppsList = document.getElementById('dynamicAppsList');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/apps');
|
const response = await fetch('/api/apps');
|
||||||
if (!response.ok) throw new Error('Nepodařilo se načíst seznam aplikací');
|
if (!response.ok) throw new Error('Nepodařilo se načíst seznam aplikací');
|
||||||
|
|
||||||
const apps = await response.json();
|
const apps = await response.json();
|
||||||
const appsList = document.getElementById('appsList');
|
|
||||||
|
|
||||||
if (apps.length === 0) {
|
if (apps.length === 0) {
|
||||||
appsList.innerHTML = '<div class="text-center text-gray-500 py-4">Žádné aplikace nebyly nalezeny.</div>';
|
dynamicAppsList.innerHTML = `
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<i class="fas fa-inbox text-4xl text-gray-300 mb-2"></i>
|
||||||
|
<p class="text-gray-500">Žádné vlastní aplikace nebyly nalezeny</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appsList.innerHTML = apps.map(app => `
|
dynamicAppsList.innerHTML = apps
|
||||||
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between" data-app-id="${app.id}">
|
.filter(app => !app.id || !app.id.startsWith('hardcoded-'))
|
||||||
<div class="flex items-center space-x-4">
|
.map(app => `
|
||||||
${app.icon ?
|
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between" data-app-id="${app.id}">
|
||||||
`<img src="/uploads/${app.icon}" alt="${app.name}" class="w-12 h-12 object-contain">` :
|
<div class="flex items-center space-x-4">
|
||||||
`<div class="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
${app.icon ?
|
||||||
<i class="fas fa-apple-alt text-gray-400 text-xl"></i>
|
`<img src="/uploads/${app.icon}" alt="${app.name}" class="w-12 h-12 object-contain">` :
|
||||||
</div>`
|
`<div class="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
||||||
}
|
<i class="fas fa-apple-alt text-gray-400 text-xl"></i>
|
||||||
<div>
|
</div>`
|
||||||
<h4 class="font-medium">${app.name}</h4>
|
}
|
||||||
<p class="text-sm text-gray-500">${app.url}</p>
|
<div>
|
||||||
|
<h4 class="font-medium">${app.name}</h4>
|
||||||
|
<p class="text-sm text-gray-500">${app.url}</p>
|
||||||
|
${app.description ? `<p class="text-sm text-gray-400">${app.description}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button class="edit-app-btn p-2 text-blue-500 hover:text-blue-700" data-app-id="${app.id}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="delete-app-btn p-2 text-red-500 hover:text-red-700" data-app-id="${app.id}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
`).join('');
|
||||||
<button class="edit-app-btn p-2 text-blue-500 hover:text-blue-700" data-app-id="${app.id}">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</button>
|
|
||||||
<button class="delete-app-btn p-2 text-red-500 hover:text-red-700" data-app-id="${app.id}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Add event listeners to buttons
|
// Add event listeners to buttons
|
||||||
document.querySelectorAll('.edit-app-btn').forEach(btn => {
|
document.querySelectorAll('.edit-app-btn').forEach(btn => {
|
||||||
@@ -1419,11 +1525,28 @@ async function loadApps() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Chyba při načítání aplikací:', error);
|
console.error('Chyba při načítání vlastních aplikací:', error);
|
||||||
showNotification('Nepodařilo se načíst seznam aplikací', 'error');
|
dynamicAppsList.innerHTML = `
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-exclamation-circle text-red-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-red-700">Chyba při načítání vlastních aplikací: ${error.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all apps (both hardcoded and dynamic)
|
||||||
|
async function loadApps() {
|
||||||
|
loadHardcodedApps();
|
||||||
|
await loadDynamicApps();
|
||||||
|
}
|
||||||
|
|
||||||
async function saveApp(event) {
|
async function saveApp(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -1431,55 +1554,126 @@ async function saveApp(event) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const appId = document.getElementById('appId').value;
|
const appId = document.getElementById('appId').value;
|
||||||
|
|
||||||
formData.append('name', document.getElementById('appName').value);
|
// Basic validation
|
||||||
formData.append('url', document.getElementById('appUrl').value);
|
const name = document.getElementById('appName').value.trim();
|
||||||
formData.append('description', document.getElementById('appDescription').value);
|
const url = document.getElementById('appUrl').value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showNotification('Název aplikace je povinný', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
showNotification('URL adresa je povinná', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('url', url);
|
||||||
|
formData.append('description', document.getElementById('appDescription').value.trim());
|
||||||
|
|
||||||
|
// Handle icon upload if a new file is selected
|
||||||
const iconInput = document.getElementById('appIcon');
|
const iconInput = document.getElementById('appIcon');
|
||||||
if (iconInput.files.length > 0) {
|
if (iconInput.files.length > 0) {
|
||||||
formData.append('icon', iconInput.files[0]);
|
formData.append('icon', iconInput.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = appId ? `/api/apps/${appId}` : '/api/apps';
|
const isEdit = !!appId;
|
||||||
const method = appId ? 'PUT' : 'POST';
|
const url = isEdit ? `/api/apps/${appId}` : '/api/apps';
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
const originalBtnText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Ukládám...';
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
// Don't set Content-Type header when using FormData, let the browser set it with the correct boundary
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText;
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
let errorMessage = 'Nepodařilo se uložit aplikaci';
|
||||||
throw new Error(error.message || 'Nepodařilo se uložit aplikaci');
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.message || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing error response:', e);
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAppModal();
|
closeAppModal();
|
||||||
await loadApps();
|
|
||||||
showNotification('Aplikace byla úspěšně uložena', 'success');
|
// Reload only dynamic apps (faster than reloading everything)
|
||||||
|
await loadDynamicApps();
|
||||||
|
|
||||||
|
showNotification(
|
||||||
|
`Aplikace byla úspěšně ${isEdit ? 'aktualizována' : 'vytvořena'}`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Chyba při ukládání aplikace:', error);
|
console.error('Chyba při ukládání aplikace:', error);
|
||||||
showNotification(error.message || 'Nepodařilo se uložit aplikaci', 'error');
|
showNotification(
|
||||||
|
error.message || 'Došlo k chybě při ukládání aplikace',
|
||||||
|
'error'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editApp(appId) {
|
async function editApp(appId) {
|
||||||
|
// Prevent editing hardcoded apps
|
||||||
|
if (appId && appId.startsWith('hardcoded-')) {
|
||||||
|
showNotification('Tuto přednastavenou aplikaci nelze upravit', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/apps/${appId}`);
|
const response = await fetch(`/api/apps/${appId}`);
|
||||||
if (!response.ok) throw new Error('Nepodařilo se načíst data aplikace');
|
if (!response.ok) throw new Error('Nepodařilo se načíst data aplikace');
|
||||||
|
|
||||||
const app = await response.json();
|
const app = await response.json();
|
||||||
|
|
||||||
|
// Set form values
|
||||||
document.getElementById('appId').value = app.id;
|
document.getElementById('appId').value = app.id;
|
||||||
document.getElementById('appName').value = app.name;
|
document.getElementById('appName').value = app.name;
|
||||||
document.getElementById('appUrl').value = app.url;
|
document.getElementById('appUrl').value = app.url;
|
||||||
document.getElementById('appDescription').value = app.description || '';
|
document.getElementById('appDescription').value = app.description || '';
|
||||||
document.getElementById('appModalTitle').textContent = 'Upravit aplikaci';
|
document.getElementById('appModalTitle').textContent = 'Upravit aplikaci';
|
||||||
|
|
||||||
|
// Update file name display
|
||||||
|
const fileNameDisplay = document.getElementById('fileName');
|
||||||
|
if (fileNameDisplay) {
|
||||||
|
fileNameDisplay.textContent = app.icon ? 'Stávající soubor: ' + app.icon : 'Nebyl vybrán žádný soubor';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show icon preview if exists
|
||||||
|
const iconPreview = document.getElementById('appIconPreview');
|
||||||
|
if (app.icon) {
|
||||||
|
iconPreview.src = `/uploads/${app.icon}`;
|
||||||
|
iconPreview.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
iconPreview.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear file input to allow re-selecting the same file
|
||||||
|
const fileInput = document.getElementById('appIcon');
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
document.getElementById('appModal').classList.remove('hidden');
|
document.getElementById('appModal').classList.remove('hidden');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1489,6 +1683,16 @@ async function editApp(appId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteApp(appId) {
|
async function deleteApp(appId) {
|
||||||
|
// Prevent deleting hardcoded apps
|
||||||
|
if (appId && appId.startsWith('hardcoded-')) {
|
||||||
|
showNotification('Tuto přednastavenou aplikaci nelze smazat', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Opravdu chcete tuto aplikaci smazat? Tato akce je nevratná.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/apps/${appId}`, {
|
const response = await fetch(`/api/apps/${appId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -1503,7 +1707,8 @@ async function deleteApp(appId) {
|
|||||||
throw new Error(error.message || 'Nepodařilo se smazat aplikaci');
|
throw new Error(error.message || 'Nepodařilo se smazat aplikaci');
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadApps();
|
// Reload only dynamic apps
|
||||||
|
await loadDynamicApps();
|
||||||
showNotification('Aplikace byla úspěšně smazána', 'success');
|
showNotification('Aplikace byla úspěšně smazána', 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1513,9 +1718,29 @@ async function deleteApp(appId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openAddAppModal() {
|
function openAddAppModal() {
|
||||||
document.getElementById('appForm').reset();
|
// Reset form
|
||||||
|
const form = document.getElementById('appForm');
|
||||||
|
if (form) form.reset();
|
||||||
|
|
||||||
|
// Clear any existing ID
|
||||||
document.getElementById('appId').value = '';
|
document.getElementById('appId').value = '';
|
||||||
|
|
||||||
|
// Update title
|
||||||
document.getElementById('appModalTitle').textContent = 'Přidat aplikaci';
|
document.getElementById('appModalTitle').textContent = 'Přidat aplikaci';
|
||||||
|
|
||||||
|
// Reset file input and preview
|
||||||
|
const fileInput = document.getElementById('appIcon');
|
||||||
|
const fileNameDisplay = document.getElementById('fileName');
|
||||||
|
const previewImg = document.getElementById('appIconPreview');
|
||||||
|
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
|
if (fileNameDisplay) fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
||||||
|
if (previewImg) {
|
||||||
|
previewImg.src = '';
|
||||||
|
previewImg.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
document.getElementById('appModal').classList.remove('hidden');
|
document.getElementById('appModal').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,6 +1748,60 @@ function closeAppModal() {
|
|||||||
document.getElementById('appModal').classList.add('hidden');
|
document.getElementById('appModal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle file input change and preview
|
||||||
|
function setupFileInput() {
|
||||||
|
const fileInput = document.getElementById('appIcon');
|
||||||
|
const fileNameDisplay = document.getElementById('fileName');
|
||||||
|
const previewImg = document.getElementById('appIconPreview');
|
||||||
|
|
||||||
|
if (!fileInput || !fileNameDisplay || !previewImg) return;
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
// Update file name display
|
||||||
|
fileNameDisplay.textContent = file.name;
|
||||||
|
|
||||||
|
// Show preview if it's an image
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
previewImg.src = e.target.result;
|
||||||
|
previewImg.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
previewImg.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
||||||
|
previewImg.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form when modal is closed
|
||||||
|
document.getElementById('appModal').addEventListener('hidden.bs.modal', function () {
|
||||||
|
const form = document.getElementById('appForm');
|
||||||
|
if (form) form.reset();
|
||||||
|
document.getElementById('appId').value = '';
|
||||||
|
const previewImg = document.getElementById('appIconPreview');
|
||||||
|
if (previewImg) {
|
||||||
|
previewImg.classList.add('hidden');
|
||||||
|
previewImg.src = '';
|
||||||
|
}
|
||||||
|
const fileNameDisplay = document.getElementById('fileName');
|
||||||
|
if (fileNameDisplay) {
|
||||||
|
fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize file input handling when the page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setupFileInput();
|
||||||
|
});
|
||||||
|
|
||||||
// Logout functionality
|
// Logout functionality
|
||||||
document.getElementById('logoutBtn').addEventListener('click', function() {
|
document.getElementById('logoutBtn').addEventListener('click', function() {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
|||||||
+70
-26
@@ -85,10 +85,10 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load and display apps
|
// Function to load and display dynamic apps
|
||||||
async function loadApps() {
|
async function loadApps() {
|
||||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||||
const appsGrid = document.querySelector('.grid.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4.gap-6');
|
const appsGrid = document.getElementById('appsGrid');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
@@ -99,24 +99,22 @@
|
|||||||
|
|
||||||
const apps = await response.json();
|
const apps = await response.json();
|
||||||
|
|
||||||
// Clear existing content
|
// Remove any existing dynamic apps (keep hardcoded ones)
|
||||||
appsGrid.innerHTML = '';
|
const dynamicApps = Array.from(appsGrid.querySelectorAll('[data-id^="dynamic-"]'));
|
||||||
|
dynamicApps.forEach(app => app.remove());
|
||||||
|
|
||||||
if (apps.length > 0) {
|
if (apps.length > 0) {
|
||||||
// Add each app to the grid
|
// Add each dynamic app to the grid
|
||||||
apps.forEach(app => {
|
apps.forEach(app => {
|
||||||
|
// Skip if this is a hardcoded app that's already displayed
|
||||||
|
const hardcodedIds = ['hardcoded-car', 'hardcoded-lunch', 'hardcoded-osticket', 'hardcoded-kanboard'];
|
||||||
|
if (hardcodedIds.includes(app.id)) return;
|
||||||
|
|
||||||
const appCard = document.createElement('div');
|
const appCard = document.createElement('div');
|
||||||
|
appCard.setAttribute('data-id', 'dynamic-' + app.id);
|
||||||
appCard.innerHTML = createAppCard(app);
|
appCard.innerHTML = createAppCard(app);
|
||||||
appsGrid.appendChild(appCard.firstElementChild);
|
appsGrid.appendChild(appCard.firstElementChild);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Show message if no apps found
|
|
||||||
appsGrid.innerHTML = `
|
|
||||||
<div class="col-span-full text-center py-8">
|
|
||||||
<i class="fas fa-inbox text-4xl text-gray-400 mb-2"></i>
|
|
||||||
<p class="text-gray-600">Žádné aplikace nebyly nalezeny</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize search functionality
|
// Initialize search functionality
|
||||||
@@ -124,13 +122,14 @@
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading apps:', error);
|
console.error('Error loading apps:', error);
|
||||||
appsGrid.innerHTML = `
|
const errorDiv = document.createElement('div');
|
||||||
<div class="col-span-full text-center py-8">
|
errorDiv.className = 'col-span-full text-center py-8';
|
||||||
<i class="fas fa-exclamation-triangle text-4xl text-red-500 mb-2"></i>
|
errorDiv.innerHTML = `
|
||||||
<p class="text-red-600">Nepodařilo se načíst aplikace</p>
|
<i class="fas fa-exclamation-triangle text-4xl text-red-500 mb-2"></i>
|
||||||
<p class="text-sm text-gray-500 mt-2">${error.message}</p>
|
<p class="text-red-600">Nepodařilo se načíst další aplikace</p>
|
||||||
</div>
|
<p class="text-sm text-gray-500 mt-2">${error.message}</p>
|
||||||
`;
|
`;
|
||||||
|
appsGrid.appendChild(errorDiv);
|
||||||
} finally {
|
} finally {
|
||||||
// Hide loading indicator
|
// Hide loading indicator
|
||||||
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
||||||
@@ -385,7 +384,7 @@
|
|||||||
<a href="http://ppc-app/pwkweb2/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Obědy</a>
|
<a href="http://ppc-app/pwkweb2/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Obědy</a>
|
||||||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -422,14 +421,59 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apps Grid -->
|
<!-- Apps Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div id="appsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<!-- Apps will be loaded here dynamically -->
|
<!-- Hardcoded apps -->
|
||||||
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-blue-600" data-name="zápis cest aut project" data-id="hardcoded-car">
|
||||||
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-blue-100 text-blue-600 mb-4">
|
||||||
|
<i class="fas fa-car-side text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Záznam služebních jízd</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Jednoduchý systém pro evidenci a správu jízd služebními vozidly.</p>
|
||||||
|
<a href="/evidence-aut" class="block text-center bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Otevřít aplikaci
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-green-600" data-name="obědy obedy jídlo lunch" data-id="hardcoded-lunch">
|
||||||
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-green-100 text-green-600 mb-4">
|
||||||
|
<i class="fas fa-utensils text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Objednávka obědů</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Portál pro objednávku a přehled firemních obědů</p>
|
||||||
|
<a href="http://ppc-app/pwkweb2/" class="block text-center bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Otevřít aplikaci
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-orange-600" data-name="osticket pomoc podpora support ticket" data-id="hardcoded-osticket">
|
||||||
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-orange-100 text-orange-600 mb-4">
|
||||||
|
<i class="fas fa-headset text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 mb-2">OSTicket</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Systém technické podpory a hlášení problémů</p>
|
||||||
|
<a href="http://osticket/" class="block text-center bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Otevřít aplikaci
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-purple-600" data-name="kanboard úkoly úkolníček tasks" data-id="hardcoded-kanboard">
|
||||||
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-purple-100 text-purple-600 mb-4">
|
||||||
|
<i class="fas fa-tasks text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Kanboard</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Správa úkolů a projektů v přehledném kanban stylu</p>
|
||||||
|
<a href="http://kanboard/" class="block text-center bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Otevřít aplikaci
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic apps will be loaded here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading indicator -->
|
<!-- Loading indicator for dynamic apps -->
|
||||||
<div id="loadingIndicator" class="text-center py-8">
|
<div id="loadingIndicator" class="text-center py-8 hidden">
|
||||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-600"></div>
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-600"></div>
|
||||||
<p class="mt-2 text-gray-600">Načítám aplikace...</p>
|
<p class="mt-2 text-gray-600">Načítám další aplikace...</p>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
/* Rounded border */
|
/* Rounded border */
|
||||||
@@ -484,7 +528,7 @@
|
|||||||
<li><a href="http://webportal/evidence-aut" class="hover:text-white">Evidence aut</a></li>
|
<li><a href="http://webportal/evidence-aut" class="hover:text-white">Evidence aut</a></li>
|
||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://localhost:8080" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -59,7 +59,7 @@
|
|||||||
<a href="http://ppc-app/pwkweb2/" class="hover:text-brand-light-blue">Objednávka obědů</a>
|
<a href="http://ppc-app/pwkweb2/" class="hover:text-brand-light-blue">Objednávka obědů</a>
|
||||||
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal/kontakt" class="hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<a href="http://ppc-app/pwkweb2/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Objednávka obědů</a>
|
<a href="http://ppc-app/pwkweb2/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Objednávka obědů</a>
|
||||||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal/kontakt" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
<li><a href="http://webportal/evidence-aut" class="hover:text-white">Evidence aut</a></li>
|
<li><a href="http://webportal/evidence-aut" class="hover:text-white">Evidence aut</a></li>
|
||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://webportal/kontakt" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -188,194 +188,460 @@ func enableCORS(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File path for storing apps
|
||||||
|
const appsFile = "data/apps.json"
|
||||||
|
|
||||||
|
// loadApps loads apps from the JSON file
|
||||||
|
func loadApps() ([]App, error) {
|
||||||
|
var apps []App
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(appsFile); os.IsNotExist(err) {
|
||||||
|
// Return empty slice if file doesn't exist
|
||||||
|
return []App{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file
|
||||||
|
data, err := os.ReadFile(appsFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading apps file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
if err := json.Unmarshal(data, &apps); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing apps JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveApps saves apps to the JSON file
|
||||||
|
func saveApps(apps []App) error {
|
||||||
|
// Create data directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(filepath.Dir(appsFile), 0755); err != nil {
|
||||||
|
return fmt.Errorf("error creating data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to pretty-printed JSON
|
||||||
|
data, err := json.MarshalIndent(apps, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling apps to JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
if err := os.WriteFile(appsFile, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("error writing apps file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// App Handlers
|
// App Handlers
|
||||||
func GetAppsHandler(w http.ResponseWriter, r *http.Request) {
|
func GetAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// In a real app, this would fetch from a database
|
apps, err := loadApps()
|
||||||
apps := []App{
|
if err != nil {
|
||||||
{
|
log.Printf("Error loading apps: %v", err)
|
||||||
ID: "1",
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
Name: "Kontakt",
|
return
|
||||||
URL: "/kontakt",
|
}
|
||||||
Description: "Kontaktní formulář",
|
|
||||||
Icon: "",
|
// Return empty array if no apps
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
if apps == nil {
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
apps = []App{}
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
// Add hardcoded apps
|
||||||
w.Header().Set("Content-Type", "application/json")
|
hardcodedApps := []App{
|
||||||
json.NewEncoder(w).Encode(apps)
|
{
|
||||||
|
ID: "hardcoded-car",
|
||||||
|
Name: "Záznam služebních jízd",
|
||||||
|
URL: "/evidence-aut",
|
||||||
|
Description: "Jednoduchý systém pro evidenci a správu jízd služebními vozidly.",
|
||||||
|
Icon: "fa-car-side",
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "hardcoded-lunch",
|
||||||
|
Name: "Objednávka obědů",
|
||||||
|
URL: "http://ppc-app/pwkweb2/",
|
||||||
|
Description: "Portál pro objednávku a přehled firemních obědů",
|
||||||
|
Icon: "fa-utensils",
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "hardcoded-osticket",
|
||||||
|
Name: "OSTicket",
|
||||||
|
URL: "http://osticket/",
|
||||||
|
Description: "Systém technické podpory a hlášení problémů",
|
||||||
|
Icon: "fa-headset",
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "hardcoded-kanboard",
|
||||||
|
Name: "Kanboard",
|
||||||
|
URL: "http://kanboard/",
|
||||||
|
Description: "Správa úkolů a projektů v přehledném kanban stylu",
|
||||||
|
Icon: "fa-tasks",
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine hardcoded and dynamic apps
|
||||||
|
allApps := append(hardcodedApps, apps...)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(allApps); err != nil {
|
||||||
|
log.Printf("Error encoding apps to JSON: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAppHandler(w http.ResponseWriter, r *http.Request) {
|
func GetAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
appID := vars["id"]
|
||||||
|
|
||||||
// In a real app, this would fetch from a database
|
// Check if it's a hardcoded app
|
||||||
if id != "1" {
|
if strings.HasPrefix(appID, "hardcoded-") {
|
||||||
http.Error(w, "App not found", http.StatusNotFound)
|
// Return 404 for non-existent hardcoded apps
|
||||||
return
|
http.NotFound(w, r)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
app := App{
|
|
||||||
ID: id,
|
// Load apps from file
|
||||||
Name: "Kontakt",
|
apps, err := loadApps()
|
||||||
URL: "/kontakt",
|
if err != nil {
|
||||||
Description: "Kontaktní formulář",
|
log.Printf("Error loading apps: %v", err)
|
||||||
Icon: "",
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
return
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
}
|
||||||
}
|
|
||||||
|
// Find the app by ID
|
||||||
w.Header().Set("Content-Type", "application/json")
|
var foundApp *App
|
||||||
json.NewEncoder(w).Encode(app)
|
for i, app := range apps {
|
||||||
|
if app.ID == appID {
|
||||||
|
foundApp = &apps[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundApp == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(foundApp); err != nil {
|
||||||
|
log.Printf("Error encoding app to JSON: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateAppHandler(w http.ResponseWriter, r *http.Request) {
|
func CreateAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Parse form data
|
// Parse form data
|
||||||
err := r.ParseMultipartForm(10 << 20) // 10 MB max file size
|
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max file size
|
||||||
if err != nil {
|
http.Error(w, "Error parsing form data", http.StatusBadRequest)
|
||||||
http.Error(w, "Error parsing form data", http.StatusBadRequest)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// Get form values
|
||||||
// Get form values
|
name := r.FormValue("name")
|
||||||
name := r.FormValue("name")
|
url := r.FormValue("url")
|
||||||
url := r.FormValue("url")
|
description := r.FormValue("description")
|
||||||
description := r.FormValue("description")
|
|
||||||
|
// Validate required fields
|
||||||
// Handle file upload
|
if name == "" || url == "" {
|
||||||
var iconPath string
|
http.Error(w, "Name and URL are required", http.StatusBadRequest)
|
||||||
file, handler, err := r.FormFile("icon")
|
return
|
||||||
if err == nil {
|
}
|
||||||
defer file.Close()
|
|
||||||
|
// Handle file upload
|
||||||
// Create uploads directory if it doesn't exist
|
var iconPath string
|
||||||
if _, err := os.Stat("uploads"); os.IsNotExist(err) {
|
file, handler, err := r.FormFile("icon")
|
||||||
os.Mkdir("uploads", 0755)
|
if err == nil {
|
||||||
}
|
defer file.Close()
|
||||||
|
|
||||||
// Generate a unique filename
|
// Create uploads directory if it doesn't exist
|
||||||
ext := ""
|
if err := os.MkdirAll("uploads", 0755); err != nil {
|
||||||
if parts := strings.Split(handler.Filename, "."); len(parts) > 1 {
|
log.Printf("Error creating uploads directory: %v", err)
|
||||||
ext = "." + parts[len(parts)-1]
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
}
|
return
|
||||||
iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
|
}
|
||||||
|
|
||||||
// Create the file
|
// Generate a unique filename with original extension
|
||||||
f, err := os.Create(filepath.Join("uploads", iconPath))
|
ext := filepath.Ext(handler.Filename)
|
||||||
if err != nil {
|
iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
|
||||||
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
|
||||||
return
|
// Create the file
|
||||||
}
|
f, err := os.Create(filepath.Join("uploads", iconPath))
|
||||||
defer f.Close()
|
if err != nil {
|
||||||
|
log.Printf("Error creating file: %v", err)
|
||||||
// Copy the uploaded file to the created file
|
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
||||||
_, err = io.Copy(f, file)
|
return
|
||||||
if err != nil {
|
}
|
||||||
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
defer f.Close()
|
||||||
return
|
|
||||||
}
|
// Copy the uploaded file to the created file
|
||||||
}
|
if _, err = io.Copy(f, file); err != nil {
|
||||||
|
log.Printf("Error copying file: %v", err)
|
||||||
// In a real app, this would save to a database
|
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
||||||
app := App{
|
return
|
||||||
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
}
|
||||||
Name: name,
|
} else if err != http.ErrMissingFile {
|
||||||
URL: url,
|
log.Printf("Error getting uploaded file: %v", err)
|
||||||
Description: description,
|
http.Error(w, "Error processing file upload", http.StatusBadRequest)
|
||||||
Icon: iconPath,
|
return
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
}
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
}
|
// Create a new app
|
||||||
|
app := App{
|
||||||
w.Header().Set("Content-Type", "application/json")
|
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||||
w.WriteHeader(http.StatusCreated)
|
Name: strings.TrimSpace(name),
|
||||||
json.NewEncoder(w).Encode(app)
|
URL: strings.TrimSpace(url),
|
||||||
|
Description: strings.TrimSpace(description),
|
||||||
|
Icon: iconPath,
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing apps
|
||||||
|
apps, err := loadApps()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error loading apps: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new app to the list
|
||||||
|
apps = append(apps, app)
|
||||||
|
|
||||||
|
// Save the updated list of apps
|
||||||
|
if err := saveApps(apps); err != nil {
|
||||||
|
log.Printf("Error saving apps: %v", err)
|
||||||
|
// Try to clean up the uploaded file if saving failed
|
||||||
|
if iconPath != "" {
|
||||||
|
if err := os.Remove(filepath.Join("uploads", iconPath)); err != nil {
|
||||||
|
log.Printf("Error cleaning up icon file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
if err := json.NewEncoder(w).Encode(app); err != nil {
|
||||||
|
log.Printf("Error encoding app to JSON: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateAppHandler(w http.ResponseWriter, r *http.Request) {
|
func UpdateAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
appID := vars["id"]
|
||||||
|
|
||||||
// In a real app, this would update in a database
|
// Prevent updating hardcoded apps
|
||||||
if id != "1" {
|
if strings.HasPrefix(appID, "hardcoded-") {
|
||||||
http.Error(w, "App not found", http.StatusNotFound)
|
http.Error(w, "Cannot update hardcoded app", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse form data
|
// Parse form data
|
||||||
err := r.ParseMultipartForm(10 << 20) // 10 MB max file size
|
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max file size
|
||||||
if err != nil {
|
http.Error(w, "Error parsing form data", http.StatusBadRequest)
|
||||||
http.Error(w, "Error parsing form data", http.StatusBadRequest)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// Load existing apps
|
||||||
// Get form values
|
apps, err := loadApps()
|
||||||
name := r.FormValue("name")
|
if err != nil {
|
||||||
url := r.FormValue("url")
|
log.Printf("Error loading apps: %v", err)
|
||||||
description := r.FormValue("description")
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
// Handle file upload if a new file is provided
|
}
|
||||||
var iconPath string
|
|
||||||
file, handler, err := r.FormFile("icon")
|
// Find the app to update
|
||||||
if err == nil {
|
var appIndex = -1
|
||||||
defer file.Close()
|
var existingApp *App
|
||||||
|
for i := range apps {
|
||||||
// Create uploads directory if it doesn't exist
|
if apps[i].ID == appID {
|
||||||
if _, err := os.Stat("uploads"); os.IsNotExist(err) {
|
appIndex = i
|
||||||
os.Mkdir("uploads", 0755)
|
existingApp = &apps[i]
|
||||||
}
|
break
|
||||||
|
}
|
||||||
// Generate a unique filename
|
}
|
||||||
ext := ""
|
|
||||||
if parts := strings.Split(handler.Filename, "."); len(parts) > 1 {
|
if appIndex == -1 {
|
||||||
ext = "." + parts[len(parts)-1]
|
http.NotFound(w, r)
|
||||||
}
|
return
|
||||||
iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
|
}
|
||||||
|
|
||||||
// Create the file
|
// Get form values
|
||||||
f, err := os.Create(filepath.Join("uploads", iconPath))
|
name := r.FormValue("name")
|
||||||
if err != nil {
|
url := r.FormValue("url")
|
||||||
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
description := r.FormValue("description")
|
||||||
return
|
|
||||||
}
|
// Handle file upload if a new file is provided
|
||||||
defer f.Close()
|
var iconPath string
|
||||||
|
file, handler, err := r.FormFile("icon")
|
||||||
// Copy the uploaded file to the created file
|
if err == nil {
|
||||||
_, err = io.Copy(f, file)
|
defer file.Close()
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
// Create uploads directory if it doesn't exist
|
||||||
return
|
if err := os.MkdirAll("uploads", 0755); err != nil {
|
||||||
}
|
log.Printf("Error creating uploads directory: %v", err)
|
||||||
}
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
// In a real app, this would update in a database
|
}
|
||||||
app := App{
|
|
||||||
ID: id,
|
// Generate a unique filename
|
||||||
Name: name,
|
ext := filepath.Ext(handler.Filename)
|
||||||
URL: url,
|
iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
|
||||||
Description: description,
|
|
||||||
Icon: iconPath, // This would be updated only if a new file was uploaded
|
// Create the file
|
||||||
CreatedAt: time.Now().Format(time.RFC3339), // In a real app, this would be fetched from the database
|
f, err := os.Create(filepath.Join("uploads", iconPath))
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
if err != nil {
|
||||||
}
|
log.Printf("Error creating file: %v", err)
|
||||||
|
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
return
|
||||||
json.NewEncoder(w).Encode(app)
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Copy the uploaded file to the created file
|
||||||
|
if _, err = io.Copy(f, file); err != nil {
|
||||||
|
log.Printf("Error copying file: %v", err)
|
||||||
|
http.Error(w, "Error saving file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old icon file if it exists and is not used by other apps
|
||||||
|
if existingApp.Icon != "" {
|
||||||
|
oldIconPath := filepath.Join("uploads", existingApp.Icon)
|
||||||
|
if _, err := os.Stat(oldIconPath); err == nil {
|
||||||
|
// Check if any other app is using this icon
|
||||||
|
iconInUse := false
|
||||||
|
for _, a := range apps {
|
||||||
|
if a.ID != appID && a.Icon == existingApp.Icon {
|
||||||
|
iconInUse = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the old icon if not in use
|
||||||
|
if !iconInUse {
|
||||||
|
if err := os.Remove(oldIconPath); err != nil {
|
||||||
|
log.Printf("Error removing old icon: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if err != http.ErrMissingFile {
|
||||||
|
log.Printf("Error getting uploaded file: %v", err)
|
||||||
|
http.Error(w, "Error processing file upload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Keep the existing icon if no new file was uploaded
|
||||||
|
iconPath = existingApp.Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app
|
||||||
|
updatedApp := App{
|
||||||
|
ID: appID,
|
||||||
|
Name: name,
|
||||||
|
URL: url,
|
||||||
|
Description: description,
|
||||||
|
Icon: iconPath,
|
||||||
|
CreatedAt: existingApp.CreatedAt, // Keep the original creation time
|
||||||
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app in the slice
|
||||||
|
apps[appIndex] = updatedApp
|
||||||
|
|
||||||
|
// Save the updated apps
|
||||||
|
if err := saveApps(apps); err != nil {
|
||||||
|
log.Printf("Error saving apps: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(updatedApp); err != nil {
|
||||||
|
log.Printf("Error encoding app to JSON: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteAppHandler(w http.ResponseWriter, r *http.Request) {
|
func DeleteAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
appID := vars["id"]
|
||||||
|
|
||||||
// In a real app, this would delete from a database
|
// Prevent deleting hardcoded apps
|
||||||
if id != "1" {
|
if strings.HasPrefix(appID, "hardcoded-") {
|
||||||
http.Error(w, "App not found", http.StatusNotFound)
|
http.Error(w, "Cannot delete hardcoded app", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
// Load existing apps
|
||||||
|
apps, err := loadApps()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error loading apps: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the app to delete
|
||||||
|
var appIndex = -1
|
||||||
|
var iconToDelete string
|
||||||
|
for i, app := range apps {
|
||||||
|
if app.ID == appID {
|
||||||
|
appIndex = i
|
||||||
|
iconToDelete = app.Icon
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appIndex == -1 {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the app from the slice
|
||||||
|
updatedApps := append(apps[:appIndex], apps[appIndex+1:]...)
|
||||||
|
|
||||||
|
// Save the updated apps
|
||||||
|
if err := saveApps(updatedApps); err != nil {
|
||||||
|
log.Printf("Error saving apps: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the app had an icon, check if it's used by any other app before deleting
|
||||||
|
if iconToDelete != "" {
|
||||||
|
// Check if any other app is using this icon
|
||||||
|
iconInUse := false
|
||||||
|
for _, app := range updatedApps {
|
||||||
|
if app.Icon == iconToDelete {
|
||||||
|
iconInUse = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the icon file if it's not in use
|
||||||
|
if !iconInUse {
|
||||||
|
iconPath := filepath.Join("uploads", iconToDelete)
|
||||||
|
if _, err := os.Stat(iconPath); err == nil {
|
||||||
|
if err := os.Remove(iconPath); err != nil {
|
||||||
|
log.Printf("Error deleting icon file: %v", err)
|
||||||
|
// Continue even if we can't delete the file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSubmit(w http.ResponseWriter, r *http.Request) {
|
func handleSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user