Infrastructure: Implement master/dev/vX branching, save git path, and fix username casing

This commit is contained in:
Daniel Bedeleanu
2026-04-10 21:51:22 +03:00
parent 93edcc261b
commit 8a3783c7e9
3397 changed files with 57402 additions and 98 deletions

110
frontend/lib/api.ts Normal file
View File

@@ -0,0 +1,110 @@
import axios from 'axios';
export const getBackendUrl = () => {
if (typeof window === 'undefined') return 'http://localhost:8000';
const host = window.location.hostname;
// If we are on HTTPS (Proxy/Mobile mode), we use port 3002 for the backend
if (window.location.protocol === 'https:') {
if (host.includes('.loca.lt')) {
return 'https://inventory-ai-api.loca.lt';
}
return `https://${host}:3002`;
}
return `http://${host}:8000`;
};
export const inventoryApi = {
getItems: async () => {
const res = await axios.get(`${getBackendUrl()}/items/`);
return res.data;
},
syncBulkOperations: async (userId: number, operations: any[]) => {
const url = `${getBackendUrl()}/operations/bulk-sync`;
try {
const res = await axios.post(url, {
user_id: userId,
operations: operations
});
return res.data;
} catch (err: any) {
console.error("Sync API Error at:", url, err);
if (err.response?.status === 404) {
throw new Error(`404: Endpoint not found at ${url}`);
}
throw err;
}
},
analyzeLabel: async (formData: FormData) => {
const res = await axios.post(`${getBackendUrl()}/items/extract-label`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
return res.data;
},
createItem: async (userId: number, itemData: any) => {
const res = await axios.post(`${getBackendUrl()}/items/`, itemData, {
params: { user_id: userId }
});
return res.data;
},
updateItem: async (itemId: number, itemData: any) => {
const res = await axios.put(`${getBackendUrl()}/items/${itemId}`, itemData);
return res.data;
},
adjustStock: async (endpoint: string, data: any) => {
const res = await axios.post(`${getBackendUrl()}/operations/${endpoint}`, data);
return res.data;
},
deleteItem: async (itemId: number) => {
const res = await axios.delete(`${getBackendUrl()}/items/${itemId}`);
return res.data;
},
getAuditLogs: async (limit: number = 50) => {
const res = await axios.get(`${getBackendUrl()}/operations/logs`, { params: { limit } });
return res.data;
},
// Users
getUsers: async () => {
const res = await axios.get(`${getBackendUrl()}/users/`);
return res.data;
},
createUser: async (userData: any) => {
const res = await axios.post(`${getBackendUrl()}/users/`, userData);
return res.data;
},
login: async (credentials: any) => {
const res = await axios.post(`${getBackendUrl()}/users/login`, credentials);
return res.data;
},
deleteUser: async (userId: number) => {
const res = await axios.delete(`${getBackendUrl()}/users/${userId}`);
return res.data;
},
// Categories
getCategories: async () => {
const res = await axios.get(`${getBackendUrl()}/categories/`);
return res.data;
},
createCategory: async (data: any) => {
const res = await axios.post(`${getBackendUrl()}/categories/`, data);
return res.data;
},
deleteCategory: async (id: number) => {
const res = await axios.delete(`${getBackendUrl()}/categories/${id}`);
return res.data;
}
};

41
frontend/lib/db.ts Normal file
View File

@@ -0,0 +1,41 @@
import Dexie, { Table } from 'dexie';
export interface Item {
id?: number;
barcode: string;
name: string;
category: string;
part_number?: string;
color?: string;
specs?: string;
quantity: number;
min_quantity: number;
image_url?: string;
labels_data?: string;
}
export interface PendingOperation {
id?: number;
type: 'CHECK_IN' | 'CHECK_OUT' | 'TRASH';
barcode: string;
quantity: number;
timestamp: number;
synced: 0 | 1; // 0 for false, 1 for true
uuid: string;
details?: string;
}
export class InventoryDatabase extends Dexie {
items!: Table<Item>;
pendingOperations!: Table<PendingOperation>;
constructor() {
super('InventoryDatabase');
this.version(3).stores({
items: '++id, barcode, name, category, part_number, color',
pendingOperations: '++id, barcode, timestamp, synced, uuid'
});
}
}
export const db = new InventoryDatabase();

62
frontend/lib/sync.ts Normal file
View File

@@ -0,0 +1,62 @@
import { db, PendingOperation } from './db';
import { inventoryApi } from './api';
export const syncOfflineOperations = async (userId: number) => {
const pending = await db.pendingOperations
.filter(op => op.synced === 0)
.toArray();
if (pending.length === 0) return { success: 0, errors: 0 };
try {
// Format operations for the backend (convert timestamp to ISO string if needed)
const formattedOps = pending.map(op => ({
type: op.type,
barcode: op.barcode,
quantity: op.quantity,
timestamp: new Date(op.timestamp).toISOString(),
uuid: op.uuid,
details: op.details
}));
const results = await inventoryApi.syncBulkOperations(userId, formattedOps);
// Mark successfully synced operations in IndexedDB
// For simplicity, if the whole bulk call succeeds, we mark all as synced or delete them
// Real implementation should match success/error per item
const successBarcodes = new Set(results.success.map((s: any) => s.barcode));
for (const op of pending) {
if (successBarcodes.has(op.barcode)) {
await db.pendingOperations.update(op.id!, { synced: 1 });
// Or remove it: await db.pendingOperations.delete(op.id!);
}
}
// Clean up synced operations
await db.pendingOperations.where('synced').equals(1).delete();
return {
success: results.success.length,
errors: results.errors.length,
details: results
};
} catch (error) {
console.error('Sync failed:', error);
throw error;
}
};
export const fetchAndCacheItems = async () => {
try {
const items = await inventoryApi.getItems();
// Update local cache
await db.items.clear();
await db.items.bulkAdd(items);
return items;
} catch (error) {
console.error('Failed to fetch items, using cache:', error);
return await db.items.toArray();
}
};