import { collection, getDocs, addDoc, setDoc, updateDoc, deleteDoc, doc, query, where, onSnapshot, serverTimestamp, Timestamp, writeBatch } from "firebase/firestore"; import { signInWithEmailAndPassword, signOut, onAuthStateChanged, createUserWithEmailAndPassword, User } from "firebase/auth"; import { ref, uploadBytes, getDownloadURL, deleteObject } from "firebase/storage"; import { db, auth, storage, handleFirestoreError, OperationType } from "./firebase"; // Collection Constant const COLLECTION_NAME = "products"; // Fully compatible type interface satisfying both the user requested structure and frontend components export interface ProductDocument { id: string; name: string; brand: string; size: string; price: number; category: "hot-selling" | "new-brands" | "famous"; segment?: "hot" | "new" | "famous"; // UI fallback field badge?: string; // UI fallback field stock: number; feature: string; imageUrl: string; image?: string; // UI fallback field description?: string; // UI fallback field createdAt?: Timestamp | Date | any; } // --------------------------------------------------------- // 9. ERROR HANDLING & NETWORK DETECTOR // --------------------------------------------------------- /** * Checks if the system browser environment has active internet connectivity */ export function isBrowserOnline(): boolean { return typeof navigator !== "undefined" ? navigator.onLine : true; } /** * Registers a callback for changes in native internet connectivity state */ export function registerNetworkStateListener(callback: (online: boolean) => void): () => void { if (typeof window === "undefined") return () => {}; const handleOnline = () => callback(true); const handleOffline = () => callback(false); window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; } // --------------------------------------------------------- // 3. CRUD FUNCTIONS (TypeScript / JavaScript Module) // --------------------------------------------------------- /** * Fills helper values into the model object so that it remains completely compatible * across the vintage UI segments & new strict collection layouts. */ function fillCompatibilityFields(data: any): any { const result = { ...data }; // Set alternative images if (result.imageUrl && !result.image) { result.image = result.imageUrl; } if (result.image && !result.imageUrl) { result.imageUrl = result.image; } // Set category/segment mapping if (result.category && !result.segment) { if (result.category === "hot-selling") result.segment = "hot"; else if (result.category === "new-brands") result.segment = "new"; else if (result.category === "famous") result.segment = "famous"; } if (result.segment && !result.category) { if (result.segment === "hot") result.category = "hot-selling"; else if (result.segment === "new") result.category = "new-brands"; else if (result.segment === "famous") result.category = "famous"; } // Supply default badges if (!result.badge) { if (result.category === "hot-selling") result.badge = "Hot Selling"; else if (result.category === "new-brands") result.badge = "New Arrival"; else result.badge = "Famous"; } // Fallback description if (!result.description) { result.description = `Premium tire profile configured for high grip and safety under high heat-cycle Pakistani road asphalt conditions.`; } // Type coercions if (result.price !== undefined) result.price = Number(result.price); if (result.stock !== undefined) result.stock = Number(result.stock); return result; } /** * Adds a new product document to Firestore 'products' collection */ export async function addProduct(productData: Omit): Promise { if (!isBrowserOnline()) { throw new Error("Unable to add product. You are currently offline."); } const payload = fillCompatibilityFields(productData); try { const docRef = await addDoc(collection(db, COLLECTION_NAME), { ...payload, createdAt: serverTimestamp() }); // Set ID on document itself await updateDoc(docRef, { id: docRef.id }); return docRef.id; } catch (error) { handleFirestoreError(error, OperationType.WRITE, COLLECTION_NAME); throw error; } } /** * Fetches all products with real-time snapshot support optionally, or via one-shot call */ export async function getAllProducts(isRetry: boolean = false): Promise { try { const q = query(collection(db, COLLECTION_NAME)); const querySnapshot = await getDocs(q); const results: ProductDocument[] = []; querySnapshot.forEach((doc) => { const data = doc.data(); results.push(fillCompatibilityFields({ ...data, id: doc.id }) as ProductDocument); }); // Check and trigger auto-seeding if collection returns empty if (results.length === 0 && !isRetry) { console.log("Empty 'products' collection found. Seeding with 12 defaults."); await seedDefaultProducts(); return await getAllProducts(true); } return results; } catch (error) { handleFirestoreError(error, OperationType.GET, COLLECTION_NAME); throw error; } } /** * Updates an existing product document */ export async function updateProduct(productId: string, updatedData: Partial): Promise { if (!isBrowserOnline()) { throw new Error("Unable to update product. You are currently offline."); } const payload = fillCompatibilityFields(updatedData); try { const docRef = doc(db, COLLECTION_NAME, productId); await updateDoc(docRef, payload); } catch (error) { handleFirestoreError(error, OperationType.WRITE, `${COLLECTION_NAME}/${productId}`); throw error; } } /** * Deletes a product from the database */ export async function deleteProduct(productId: string): Promise { if (!isBrowserOnline()) { throw new Error("Unable to delete product. You are currently offline."); } try { await deleteDoc(doc(db, COLLECTION_NAME, productId)); } catch (error) { handleFirestoreError(error, OperationType.DELETE, `${COLLECTION_NAME}/${productId}`); throw error; } } /** * Increases or decreases all prices in the inventory database by a given percentage. * Example: bulkUpdatePrices(10) increases all rates by 10%, while bulkUpdatePrices(-5) cuts them by 5%. */ export async function bulkUpdatePrices(percentage: number): Promise { if (!isBrowserOnline()) { throw new Error("No network connectivity. Bulk operations are disabled."); } try { const q = query(collection(db, COLLECTION_NAME)); const querySnapshot = await getDocs(q); const batch = writeBatch(db); let count = 0; querySnapshot.forEach((document) => { const data = document.data() as ProductDocument; const originalPrice = Number(data.price) || 0; const multiplier = 1 + (percentage / 100); const newPrice = Math.round(originalPrice * multiplier); batch.update(doc(db, COLLECTION_NAME, document.id), { price: newPrice }); count++; }); if (count > 0) { await batch.commit(); console.log(`Successfully completed bulk price update on ${count} products.`); } } catch (error) { handleFirestoreError(error, OperationType.WRITE, `${COLLECTION_NAME}/bulkUpdate`); throw error; } } /** * Returns products with stock levels strictly below the threshold value */ export async function getLowStockProducts(threshold: number = 5): Promise { try { const q = query(collection(db, COLLECTION_NAME), where("stock", "<", threshold)); const querySnapshot = await getDocs(q); const results: ProductDocument[] = []; querySnapshot.forEach((doc) => { results.push(fillCompatibilityFields({ ...doc.data(), id: doc.id })); }); return results; } catch (error) { handleFirestoreError(error, OperationType.GET, COLLECTION_NAME); throw error; } } /** * Searches the collection for products with matching names or manufacturer brands */ export async function searchProducts(searchQuery: string): Promise { // Firestore does not natively support multi-field partial matching easily in client SDK, // so we fetch all and apply high-performance filter locally to ensure maximum reliability and speed. try { const all = await getAllProducts(); const queryLower = searchQuery.toLowerCase().trim(); if (!queryLower) return all; return all.filter(p => p.name.toLowerCase().includes(queryLower) || p.brand.toLowerCase().includes(queryLower) || p.size.toLowerCase().includes(queryLower) ); } catch (error) { throw error; } } /** * Filter products specifically by database category classification */ export async function filterByCategory(category: "hot-selling" | "new-brands" | "famous"): Promise { try { const q = query(collection(db, COLLECTION_NAME), where("category", "==", category)); const querySnapshot = await getDocs(q); const results: ProductDocument[] = []; querySnapshot.forEach((doc) => { results.push(fillCompatibilityFields({ ...doc.data(), id: doc.id })); }); return results; } catch (error) { handleFirestoreError(error, OperationType.GET, COLLECTION_NAME); throw error; } } // --------------------------------------------------------- // 4. AUTHENTICATION FUNCTIONS // --------------------------------------------------------- /** * Authenticates an administrator using standard email and password */ export async function loginAdmin(email: string, password: string): Promise { try { const userCredential = await signInWithEmailAndPassword(auth, email, password); return userCredential.user; } catch (error: any) { console.error("Login attempt failed:", error); throw new Error(error.message || "Invalid authentication credentials."); } } /** * Standard security signout for administrators */ export async function logoutAdmin(): Promise { try { await signOut(auth); } catch (error: any) { throw new Error(error.message || "Signout failed."); } } /** * Attaches a state change listener tracking authentications */ export function checkAuthState(callback: (user: User | null) => void): () => void { return onAuthStateChanged(auth, (user) => { callback(user); }); } /** * Utility to register fresh admin personnel accounts */ export async function createAdminUser(email: string, password: string): Promise { try { const userCredential = await createUserWithEmailAndPassword(auth, email, password); return userCredential.user; } catch (error: any) { throw new Error(error.message || "Failed to create administrator user."); } } // --------------------------------------------------------- // 5. STORAGE FUNCTIONS // --------------------------------------------------------- /** * Uploads a tire image binary file to the Firebase Storage bucket */ export async function uploadProductImage(file: File, productName: string): Promise { if (!isBrowserOnline()) { throw new Error("Unable to upload image. Network is currently offline."); } try { const cleanName = productName.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); const timestamp = Date.now(); const storageRef = ref(storage, `product-images/${cleanName}_${timestamp}`); const snapshot = await uploadBytes(storageRef, file); const downloadUrl = await getDownloadURL(snapshot.ref); return downloadUrl; } catch (error: any) { console.error("Storage image upload failure:", error); throw new Error(error.message || "Failed to store product asset image."); } } /** * Deletes standard file objects from the Firebase storage bucket using their download addresses */ export async function deleteProductImage(imageUrl: string): Promise { if (!isBrowserOnline()) { throw new Error("Unable to deleted image. Offline mode."); } try { // Extract file reference path from firebase download URI const decodedUrl = decodeURIComponent(imageUrl); const relativePathStart = decodedUrl.indexOf("/o/") + 3; const relativePathEnd = decodedUrl.indexOf("?alt="); if (relativePathStart > 2 && relativePathEnd > relativePathStart) { const filePath = decodedUrl.substring(relativePathStart, relativePathEnd); const fileRef = ref(storage, filePath); await deleteObject(fileRef); } } catch (error: any) { console.warn("Storage item cleanup ignored (possibly using an external image link):", error.message); } } // --------------------------------------------------------- // 7. SEED DATA FUNCTION // --------------------------------------------------------- // Exhaustive real Pakistan market default dataset encompassing exactly 12 products export const DEFAULT_TIR_PRODUCTS_SEED: Omit[] = [ // --- HOT SELLING --- { name: "Autogrip F107 Comfort Grip", brand: "Autogrip", size: "185/65R14", price: 12500, category: "hot-selling", stock: 15, feature: "All-season wet grip & stability - 185/65R14", imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg", description: "Designed for premium everyday passenger cars. Delivers superior compound performance in all seasons with low road noise levels. Size: 185/65R14." }, { name: "Autogrip SUV A606 Rugged", brand: "Autogrip", size: "235/60R18", price: 22000, category: "hot-selling", stock: 4, // low stock flag feature: "Off-road rugged tread design - 235/60R18", imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg", description: "Aggressive tread blocks and heavy-duty compound for SUVs. Provides high durability on rugged tracks, gravel, and highway driving alike. Size: 235/60R18." }, { name: "General Tire Grabber AT3", brand: "General Tire", size: "265/65R17", price: 28500, category: "hot-selling", stock: 8, feature: "All-terrain robust capability - 265/65R17", imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg", description: "Developed to meet the needs of SUV, pick-up truck and off-road vehicle drivers who want high command and high-traction performance. Size: 265/65R17." }, { name: "Bridgestone Turanza T005", brand: "Bridgestone", size: "205/55R16", price: 21000, category: "hot-selling", stock: 12, feature: "Premium comfort & low noise rating - 205/55R16", imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg", description: "Flagship luxury touring tire providing superior control in wet and dry conditions, with fuel efficiency optimization. Size: 205/55R16." }, // --- NEW BRANDS / NEW ARRIVALS --- { name: "Michelin Pilot Sport 5", brand: "Michelin", size: "225/45R17", price: 35000, category: "new-brands", stock: 3, // low stock flag feature: "Ultra-high performance & control - 225/45R17", imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg", description: "Live each driving moment to the fullest. Dynamic response technology ensures high precision handling and short braking distances. Size: 225/45R17." }, { name: "Pirelli Cinturato P7 Eco", brand: "Pirelli", size: "205/60R16", price: 24500, category: "new-brands", stock: 7, feature: "Eco-friendly, low rolling resistance - 205/60R16", imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg", description: "Green performance luxury tire developed for modern cars. High safety levels, low fuel consumption, and eco-friendly raw materials. Size: 205/60R16." }, { name: "Yokohama Geolandar CV G058", brand: "Yokohama", size: "215/70R16", price: 26000, category: "new-brands", stock: 9, feature: "Crossover/SUV quiet touring - 215/70R16", imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg", description: "Formidable wet grip with advanced CV silica compound. Designed to deliver confident handling and long wear life for modern crossovers. Size: 215/70R16." }, { name: "Continental PremiumContact 7", brand: "Continental", size: "195/55R15", price: 19500, category: "new-brands", stock: 14, feature: "Outstanding safety & wet braking - 195/55R15", imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg", description: "Experience absolute driving comfort and peace of mind. Outstanding safety on wet roads and superior wet braking efficiency. Size: 195/55R15." }, // --- BESTSELLING GLOBAL / FAMOUS Products --- { name: "Dunlop Sport Maxx RT2", brand: "Dunlop", size: "225/40R18", price: 32000, category: "famous", stock: 2, // low stock flag feature: "Ultra-high performance sport traction - 225/40R18", imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg", description: "Champion grip and braking performance. Motorsport-derived compound offers supreme steering precision and curve cornering. Size: 225/40R18." }, { name: "Goodyear Eagle F1 Asymmetric 6", brand: "Goodyear", size: "235/35R19", price: 42000, category: "famous", stock: 11, feature: "Max grip, sports performance - 235/35R19", imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg", description: "Ready for anything. Adaptable contact patch ensures extreme asphalt feedback. Excellent performance in both wet and dry tracks. Size: 235/35R19." }, { name: "Hankook Ventus S1 evo3 Elite", brand: "Hankook", size: "225/40R18", price: 30000, category: "famous", stock: 6, feature: "Luxury sports OE high response rating - 225/40R18", imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg", description: "Original Equipment (OE) for BMW, Audi, and Mercedes. Highlights high response speed, high durability, and lower rolling noise. Size: 225/40R18." }, { name: "Autogrip Winter Grip W609 Extreme", brand: "Autogrip", size: "195/65R15", price: 14000, category: "famous", stock: 5, feature: "Extreme snow & water grip traction - 195/65R15", imageUrl: "/src/assets/images/winter_tire_1781509513584.jpg", description: "Heavy-siped tread design that grips perfectly during heavy rain, wet mud, and cold winter situations. Ensures reliable road security. Size: 195/65R15." } ]; /** * Checks if the products collection is empty and fills it with 12 premium tyres if needed */ export async function seedDefaultProducts(): Promise { try { const q = query(collection(db, COLLECTION_NAME)); const querySnapshot = await getDocs(q); if (!querySnapshot.empty) { console.log("Database 'products' collection is already populated. Seeding aborted."); return; } console.log("Initiating premium 12 product configurations cloud seed operation."); // Batch write to optimize bandwidth const batch = writeBatch(db); for (let i = 0; i < DEFAULT_TIR_PRODUCTS_SEED.length; i++) { const item = DEFAULT_TIR_PRODUCTS_SEED[i]; const cleanBrand = item.brand.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-"); const customId = `seed-${cleanBrand}-${i + 1}`; const payload = fillCompatibilityFields({ ...item, id: customId, createdAt: new Date() }); const docRef = doc(db, COLLECTION_NAME, customId); batch.set(docRef, payload); } await batch.commit(); console.log("Database seeded successfully with exactly 12 default tires!"); } catch (error) { console.error("Failure while running cloud seeding:", error); } } // --------------------------------------------------------- // 8. REAL-TIME LISTENERS // --------------------------------------------------------- /** * Sets up a live listener on the main collections. Realizes reactive UI state * updates on item modifications. */ export function onProductsSnapshot(callback: (products: ProductDocument[]) => void): () => void { const q = query(collection(db, COLLECTION_NAME)); return onSnapshot(q, (snapshot) => { const results: ProductDocument[] = []; snapshot.forEach((doc) => { const data = doc.data(); results.push(fillCompatibilityFields({ ...data, id: doc.id }) as ProductDocument); }); callback(results); }, (error) => { handleFirestoreError(error, OperationType.LIST, COLLECTION_NAME); }); } // --------------------------------------------------------- // 10. EXPORT / IMPORT FUNCTIONS // --------------------------------------------------------- /** * Downloads the complete active database catalog as a structured JSON file instantly from the browser */ export async function exportToJSON(): Promise { try { const allProducts = await getAllProducts(); const dataString = JSON.stringify(allProducts, null, 2); // Trigger localized file download in client runtime const blob = new Blob([dataString], { type: "application/json" }); const fileAnchor = document.createElement("a"); fileAnchor.href = URL.createObjectURL(blob); fileAnchor.download = `haider_brothers_products_backup_${Date.now()}.json`; document.body.appendChild(fileAnchor); fileAnchor.click(); document.body.removeChild(fileAnchor); } catch (error) { console.error("Export file creation error: ", error); alert("Export operation encountered failures. Check network status."); } } /** * Uploads a user supplied JSON backup file, checks for duplicates, and uploads novel configurations */ export async function importFromJSON(jsonFile: File): Promise<{ imported: number; duplicates: number }> { if (!isBrowserOnline()) { throw new Error("Unable to import product index while offline."); } return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async (event) => { try { const fileContent = event.target?.result; if (typeof fileContent !== "string") { throw new Error("Invalid file readings."); } const parsedData = JSON.parse(fileContent); const productsToImport: any[] = Array.isArray(parsedData) ? parsedData : [parsedData]; // Fetch existing configurations to handle duplicate prevention by names and size matching const existingProducts = await getAllProducts(); let importCount = 0; let duplicateCount = 0; const batch = writeBatch(db); for (const item of productsToImport) { // Normalize names & sizes check for solid uniqueness check const isDuplicate = existingProducts.some(ep => ep.name.toLowerCase().trim() === item.name?.toLowerCase().trim() && ep.size.toLowerCase().trim() === item.size?.toLowerCase().trim() ); if (isDuplicate) { duplicateCount++; continue; } const uniqueId = item.id || `imported-${Date.now()}-${Math.floor(Math.random() * 1000)}`; const payload = fillCompatibilityFields({ ...item, id: uniqueId, createdAt: new Date() }); const docRef = doc(db, COLLECTION_NAME, uniqueId); batch.set(docRef, payload); importCount++; } if (importCount > 0) { await batch.commit(); } resolve({ imported: importCount, duplicates: duplicateCount }); } catch (error: any) { console.error("Failed to parse or save backup file:", error); reject(new Error(error.message || "Invalid JSON or database security rules violation during upload.")); } }; reader.onerror = () => reject(new Error("File read error.")); reader.readAsText(jsonFile); }); } // --------------------------------------------------------- // 11. VISITOR TRACKING & TRAFFIC METRICS // --------------------------------------------------------- function getOrCreateSessionId(): string { if (typeof window === "undefined" || !window.sessionStorage) { return "session-fallback"; } let sid = window.sessionStorage.getItem("hb_visitor_sid"); if (!sid) { sid = "sid-" + Math.random().toString(36).substring(2, 15) + "-" + Date.now(); window.sessionStorage.setItem("hb_visitor_sid", sid); } return sid; } /** * Logs a standard visitor page view event into the Firestore visitors collection */ export async function logVisitorInfo(pagePath: string): Promise { if (typeof window === "undefined") return; try { const sessionId = getOrCreateSessionId(); const userAgent = window.navigator.userAgent || "Unknown UA"; const language = window.navigator.language || "en"; // Quick OS signature parsing let platform = "Other / Web"; const ua = userAgent.toLowerCase(); if (ua.includes("windows")) platform = "Windows"; else if (ua.includes("macintosh") || ua.includes("mac os")) platform = "macOS"; else if (ua.includes("android")) platform = "Android"; else if (ua.includes("iphone") || ua.includes("ipad")) platform = "iOS"; else if (ua.includes("linux")) platform = "Linux"; // Build unique document key const customId = `vis-${sessionId.substring(4, 16)}-${Date.now()}`; const docRef = doc(db, "visitors", customId); await setDoc(docRef, { sessionId, userAgent: userAgent.substring(0, 500), platform, language, pagePath, timestamp: serverTimestamp() }); } catch (error) { // Fail silently so analytics tracking never disrupts primary showroom client flow console.warn("Analytics logging bypassed:", error); } } export interface VisitorRecord { id: string; sessionId: string; userAgent: string; platform: string; language: string; pagePath: string; timestamp: any; } /** * Retrieves the sequential log of website visitation reports */ export async function getVisitorLogs(): Promise { try { const q = query(collection(db, "visitors")); const querySnapshot = await getDocs(q); const results: VisitorRecord[] = []; querySnapshot.forEach((doc) => { const data = doc.data(); results.push({ id: doc.id, sessionId: data.sessionId || "", userAgent: data.userAgent || "", platform: data.platform || "", language: data.language || "", pagePath: data.pagePath || "", timestamp: data.timestamp }); }); // Order chronologically by descending timestamp order results.sort((a, b) => { const tA = a.timestamp?.seconds || 0; const tB = b.timestamp?.seconds || 0; return tB - tA; }); return results; } catch (error) { handleFirestoreError(error, OperationType.GET, "visitors"); throw error; } } /** * Adds a new email address to the newsletter subscribers list in Firestore. */ export async function subscribeNewsletter(email: string): Promise { const collectionName = "subscribers"; if (!isBrowserOnline()) { throw new Error("Unable to subscribe. You are currently offline."); } const cleanEmail = email.trim().toLowerCase(); const emailRegex = /^[^@]+@[^@]+\.[^@]+$/; if (!emailRegex.test(cleanEmail)) { throw new Error("Please present a valid email format."); } try { // Build a secure document ID based on the email (replaces special characters) matching the rules regex limit. const customId = `sub-${cleanEmail.replace(/[^a-zA-Z0-9_-]/g, "_")}`.substring(0, 128); const docRef = doc(db, collectionName, customId); await setDoc(docRef, { email: cleanEmail, createdAt: serverTimestamp() }); } catch (error) { handleFirestoreError(error, OperationType.WRITE, collectionName); throw error; } }