Add Phase 2: Local SQLite database with sync functionality
Implements complete offline-first architecture with SQLite caching and MariaDB synchronization. Key features: - Local SQLite database for offline operation (data/quoteforge.db) - Connection settings with encrypted credentials - Component and pricelist caching with auto-sync - Sync API endpoints (/api/sync/status, /components, /pricelists, /all) - Real-time sync status indicator in UI with auto-refresh - Offline mode detection middleware - Migration tool for database initialization - Setup wizard for initial configuration New components: - internal/localdb: SQLite repository layer (components, pricelists, sync) - internal/services/sync: Synchronization service - internal/handlers/sync: Sync API handlers - internal/handlers/setup: Setup wizard handlers - internal/middleware/offline: Offline detection - cmd/migrate: Database migration tool UI improvements: - Setup page for database configuration - Sync status indicator with online/offline detection - Warning icons for pending synchronization - Auto-refresh every 30 seconds Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -187,21 +187,11 @@ async function loadTab(tab) {
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('tab-content').innerHTML = '<div class="text-center py-8 text-gray-500">Загрузка...</div>';
|
||||
|
||||
try {
|
||||
if (currentTab === 'alerts') {
|
||||
const resp = await fetch('/admin/pricing/alerts?per_page=100', {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (resp.status === 401) { logout(); return; }
|
||||
if (resp.status === 403) { window.location.href = '/'; return; }
|
||||
const resp = await fetch('/api/admin/pricing/alerts?per_page=100');
|
||||
const data = await resp.json();
|
||||
renderAlerts(data.alerts || []);
|
||||
} else if (currentTab === 'all-configs') {
|
||||
@@ -210,17 +200,13 @@ async function loadData() {
|
||||
if (currentSearch) {
|
||||
url += '&search=' + encodeURIComponent(currentSearch);
|
||||
}
|
||||
const resp = await fetch(url, {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (resp.status === 401) { logout(); return; }
|
||||
if (resp.status === 403) { window.location.href = '/'; return; }
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
totalPages = Math.ceil(data.total / perPage);
|
||||
renderAllConfigs(data.configurations || []);
|
||||
updatePagination(data.total);
|
||||
} else {
|
||||
let url = '/admin/pricing/components?page=' + currentPage + '&per_page=' + perPage;
|
||||
let url = '/api/admin/pricing/components?page=' + currentPage + '&per_page=' + perPage;
|
||||
if (currentSearch) {
|
||||
url += '&search=' + encodeURIComponent(currentSearch);
|
||||
}
|
||||
@@ -230,10 +216,7 @@ async function loadData() {
|
||||
if (sortDir) {
|
||||
url += '&dir=' + encodeURIComponent(sortDir);
|
||||
}
|
||||
const resp = await fetch(url, {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (resp.status === 401) { logout(); return; }
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
totalPages = Math.ceil(data.total / perPage);
|
||||
componentsCache = data.components || [];
|
||||
@@ -471,9 +454,6 @@ function onMethodChange() {
|
||||
}
|
||||
|
||||
async function fetchPreview() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const lotName = document.getElementById('modal-lot-name').value;
|
||||
const method = document.getElementById('modal-method').value;
|
||||
const periodDays = parseInt(document.getElementById('modal-period').value) || 0;
|
||||
@@ -490,10 +470,9 @@ async function fetchPreview() {
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/admin/pricing/preview', {
|
||||
const resp = await fetch('/api/admin/pricing/preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -508,8 +487,6 @@ async function fetchPreview() {
|
||||
})
|
||||
});
|
||||
|
||||
if (resp.status === 401) { logout(); return; }
|
||||
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -584,12 +561,6 @@ function debounceFetchPreview() {
|
||||
}
|
||||
|
||||
async function savePrice() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const lotName = document.getElementById('modal-lot-name').value;
|
||||
const method = document.getElementById('modal-method').value;
|
||||
const periodDaysStr = document.getElementById('modal-period').value;
|
||||
@@ -630,17 +601,14 @@ async function savePrice() {
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/admin/pricing/update', {
|
||||
const resp = await fetch('/api/admin/pricing/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (resp.status === 401) { logout(); return; }
|
||||
|
||||
if (resp.ok) {
|
||||
closeModal();
|
||||
loadData();
|
||||
@@ -683,12 +651,6 @@ function processMetaPrices(metaPrices, originalLotName) {
|
||||
}
|
||||
|
||||
function recalculateAll() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btn-recalc');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
@@ -707,9 +669,8 @@ function recalculateAll() {
|
||||
progressStats.textContent = 'Подготовка...';
|
||||
|
||||
// Use fetch with streaming for SSE
|
||||
fetch('/admin/pricing/recalculate-all', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
fetch('/api/admin/pricing/recalculate-all', {
|
||||
method: 'POST'
|
||||
}).then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
Reference in New Issue
Block a user