Panoramica del servizio
Sgomber24 ti offre un servizio professionale di traslochi e sgomberi, garantendo rapidità, sicurezza e prezzi trasparenti per ogni esigenza.

Traslochi su misura
Organizziamo traslochi personalizzati, curando ogni dettaglio per assicurare un trasferimento senza stress e puntuale.
Sgomberi rapidi e sicuri
Eseguiamo sgomberi efficienti con smaltimento responsabile, liberando spazi in tempi brevi e senza compromessi.
Preventivi gratuiti e trasparenti
Confronta facilmente offerte delle migliori aziende locali, scegliendo il servizio che unisce qualità e convenienza.
Trova il traslocatore perfetto per ogni tuo bisogno
Scopri come ottenere un preventivo rapido e dettagliato per i nostri servizi di sgombero e trasloco, con semplicità e trasparenza.
Via Roma 88, Interno 5
Via Dante 34, Palazzo B
+39 06 1234 5678
contatti@sgomber24.it

Come Funziona il Servizio
Scopri il metodo semplice e veloce per trovare traslocatori professionali e organizzare sgomberi senza stress, passo dopo passo.
Passo Uno: Richiedi Preventivi
Inserisci i dettagli del tuo trasloco o sgombero per ricevere offerte personalizzate dalle migliori aziende locali.
Passaggio Due: Confronta Opzioni
Valuta i preventivi ricevuti per scegliere il servizio più affidabile e conveniente in base alle tue esigenze.
Passaggio Tre: Prenota il Servizio
Finalizza la tua scelta e coordina con il team selezionato per un trasloco o sgombero rapido e senza intoppi.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sgomberi24 Sgomberi Professionali</title>
<!-- Caricamento di Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Configurazione del font Inter e stile base -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f7f9fc;
}
.container-shadow {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body>
<div id="app" class="min-h-screen">
<!-- Header e Navigazione -->
<header class="bg-indigo-700 text-white shadow-lg p-4">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<h1 class="text-3xl font-bold">Sgomberi24.IT</h1>
<nav class="flex space-x-4">
<button onclick="changeView('client')" id="client-view-btn" class="py-2 px-4 rounded-full transition duration-300 bg-indigo-500 hover:bg-indigo-600">Sono un Cliente</button>
<button onclick="changeView('professional')" id="professional-view-btn" class="py-2 px-4 rounded-full transition duration-300 hover:bg-indigo-600">Sono un Professionista</button>
<button onclick="changeView('profile')" id="profile-view-btn" class="py-2 px-4 rounded-full transition duration-300 hover:bg-indigo-600 bg-gray-600">Gestisci Profilo</button>
</nav>
</div>
</header>
<!-- Area Principale -->
<main class="max-w-7xl mx-auto p-4 md:p-8">
<!-- Stato di Autenticazione e ID Utente -->
<div id="auth-info" class="mb-6 p-4 bg-white rounded-lg container-shadow">
<p class="text-sm text-gray-600">
Stato: <span id="auth-status" class="font-semibold text-red-500">In attesa di autenticazione...</span>
</p>
<p class="text-sm text-gray-600 mt-1">
Il tuo ID Utente per la collaborazione: <code id="user-id-display" class="font-mono bg-gray-100 p-1 rounded text-xs text-indigo-700 break-all">Caricamento...</code>
</p>
</div>
<!-- Contenuto Dinamico -->
<div id="content-area">
<!-- Viste Verranno Caricate Qui -->
</div>
</main>
<!-- Modale di Messaggio (per sostituire alert/confirm) -->
<div id="modal" class="fixed inset-0 bg-gray-900 bg-opacity-75 hidden items-center justify-center p-4 z-50">
<div class="bg-white rounded-xl container-shadow p-6 w-full max-w-sm">
<h3 id="modal-title" class="text-xl font-bold mb-3 text-indigo-700"></h3>
<p id="modal-message" class="text-gray-700 mb-4"></p>
<button onclick="closeModal()" class="w-full bg-indigo-600 text-white py-2 rounded-lg hover:bg-indigo-700 transition duration-300">Chiudi</button>
</div>
</div>
<!-- Galleria Modale per Immagini -->
<div id="gallery-modal" class="fixed inset-0 bg-gray-900 bg-opacity-95 hidden items-center justify-center p-4 z-50">
<div class="relative w-full h-full max-w-4xl max-h-[90vh]">
<!-- Bottone Chiudi -->
<button onclick="closeGallery()" class="absolute top-4 right-4 text-white text-3xl z-50 bg-black/50 p-2 rounded-full hover:bg-black/80 transition">
×
</button>
<!-- Immagine Corrente -->
<div id="gallery-image-container" class="w-full h-full flex items-center justify-center">
<img id="current-gallery-image" src="" alt="Anteprima Immagine Allegata" class="max-w-full max-h-full object-contain rounded-lg shadow-2xl">
</div>
<!-- Bottone Precedente -->
<button onclick="navigateGallery(-1)" id="prev-btn" class="absolute left-4 top-1/2 transform -translate-y-1/2 text-white text-4xl bg-black/50 p-3 rounded-full hover:bg-black/80 transition disabled:opacity-30 disabled:cursor-not-allowed">
❮
</button>
<!-- Bottone Successivo -->
<button onclick="navigateGallery(1)" id="next-btn" class="absolute right-4 top-1/2 transform -translate-y-1/2 text-white text-4xl bg-black/50 p-3 rounded-full hover:bg-black/80 transition disabled:opacity-30 disabled:cursor-not-allowed">
❯
</button>
</div>
</div>
<!-- Fine Galleria Modale -->
<!-- Componente di Caricamento -->
<div id="loading-overlay" class="fixed inset-0 bg-gray-100 bg-opacity-75 flex items-center justify-center hidden z-40">
<div class="flex flex-col items-center">
<svg class="animate-spin -ml-1 mr-3 h-8 w-8 text-indigo-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="mt-3 text-indigo-700 font-semibold">Caricamento dati in tempo reale...</p>
</div>
</div>
</div>
<!-- Moduli e Logica Firebase -->
<script type="module">
// Importazioni Firebase
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, getDoc, setDoc, onSnapshot, collection, query, addDoc, updateDoc, serverTimestamp, orderBy, getDocs, where } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
import { setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// Variabili Globali
let app;
let db;
let auth;
let userId = null;
let userName = "Utente Anonimo"; // Nome di default
let userProfile = {}; // NEW: Profilo utente caricato dal DB
let currentView = 'client';
// Variabili fornite dall'ambiente (MANDATORY USE)
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-prolink-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// --- Dati Statici per la Homepage ---
const SERVICE_CATEGORIES = [
"Sgombero e Svuoto (Cantine, Box, Soffitte)",
"Sgombero Uffici, Negozi e Magazzini",
"Smontaggio e Montaggio Mobili (Pre/Post Trasloco)",
"Pulizie di Sgrosso Finali (Post Trasloco o Ristrutturazione)",
"Trasporti e Logistica Leggera",
"Gestione e Smaltimento Rifiuti Ingombranti (Simile a LoveJunk)"
];
const SERVICE_REGIONS = [
{ region: "Lombardia", cities: ["Milano", "Bergamo", "Brescia", "Monza"] },
{ region: "Lazio", cities: ["Roma", "Frosinone", "Latina", "Viterbo"] },
{ region: "Campania", cities: ["Napoli", "Salerno", "Caserta", "Avellino"] },
{ region: "Piemonte", cities: ["Torino", "Novara", "Alessandria"] },
{ region: "Emilia-Romagna", cities: ["Bologna", "Modena", "Parma"] },
{ region: "Veneto", cities: ["Venezia", "Verona", "Padova"] }
];
// --- Gallery State ---
let currentImages = [];
let currentIndex = 0;
// Configurazione e Inizializzazione Firebase
function initializeFirebase() {
if (Object.keys(firebaseConfig).length === 0) {
console.error("Firebase config non disponibile. L'app non funzionerà in modo persistente.");
return;
}
try {
// setLogLevel('Debug'); // Per vedere i log di Firestore in console
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
// 1. Autenticazione
onAuthStateChanged(auth, async (user) => {
if (user) {
userId = user.uid;
// 2. Carica o crea il profilo utente nel DB
await checkOrCreateUserProfile(userId);
document.getElementById('user-id-display').textContent = userId;
document.getElementById('auth-status').textContent = 'Connesso';
document.getElementById('auth-status').classList.remove('text-red-500');
document.getElementById('auth-status').classList.add('text-green-500');
// Una volta autenticato, avvia i listeners
startDataListeners();
changeView(currentView); // Ricarica la vista per mostrare i dati
} else {
// 3. Tenta l'accesso con custom token o anonimo
try {
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
} catch (error) {
console.error("Errore nell'autenticazione:", error);
document.getElementById('auth-status').textContent = `Errore (${error.code})`;
showModal('Errore di Autenticazione', 'Non è stato possibile accedere a Firebase. Controlla la console per i dettagli.');
}
}
});
} catch (error) {
console.error("Errore nell'inizializzazione di Firebase:", error);
showModal('Errore Critico', 'Non è stato possibile inizializzare l'app Firebase. Controlla la console.');
}
}
// --- Gestione Profilo Utente (Nuove Funzioni DB) ---
function getUsersCollectionPath() {
return getPublicCollectionPath('users');
}
function getFullName(profile) {
if (profile.firstName && profile.lastName) {
return `${profile.firstName} ${profile.lastName}`;
}
return profile.name || `Utente_${profile.id.substring(0, 8)}`;
}
async function checkOrCreateUserProfile(uid) {
const userDocRef = doc(db, getUsersCollectionPath(), uid);
try {
const docSnap = await getDoc(userDocRef);
if (docSnap.exists()) {
userProfile = { id: uid, ...docSnap.data() };
userName = getFullName(userProfile);
console.log("Profilo utente caricato:", userProfile);
} else {
// Crea un profilo base se non esiste
userProfile = {
id: uid,
name: `Utente_${uid.substring(0, 8)}`,
firstName: '',
lastName: '',
address: '',
city: '',
zip: '',
phone: '',
createdAt: serverTimestamp(),
// Dati specifici per il professionista
professionalProfile: {
isProfessional: false,
specialization: "",
bio: "Ancora nessuna biografia.",
rating: 0,
}
};
await setDoc(userDocRef, userProfile);
userName = getFullName(userProfile);
console.log("Nuovo profilo utente creato.");
}
} catch (e) {
console.error("Errore nel caricare/creare il profilo utente:", e);
showModal('Errore Profilo', 'Impossibile accedere ai dati del profilo. Riprova più tardi.');
}
}
window.saveProfessionalProfile = async (event) => {
event.preventDefault();
if (!userId) {
showModal('Errore', 'Utente non autenticato.');
return;
}
// Dati anagrafici (Nuovi campi)
const firstName = document.getElementById('first-name').value.trim();
const lastName = document.getElementById('last-name').value.trim();
const address = document.getElementById('address').value.trim();
const city = document.getElementById('city').value.trim();
const zip = document.getElementById('zip').value.trim();
const phone = document.getElementById('phone').value.trim();
// Dati professionali
const specialization = document.getElementById('prof-specialization').value.trim();
const bio = document.getElementById('prof-bio').value.trim();
const isProfessional = document.getElementById('is-professional-checkbox').checked;
showLoading();
try {
const userDocRef = doc(db, getUsersCollectionPath(), userId);
// Aggiorna tutti i campi
const newProfileData = {
// Anagrafica
firstName: firstName,
lastName: lastName,
address: address,
city: city,
zip: zip,
phone: phone,
// Nome visualizzato
name: getFullName({ firstName, lastName, id: userId }),
// Dati professionali
professionalProfile: {
isProfessional: isProfessional,
specialization: specialization,
bio: bio,
rating: userProfile.professionalProfile?.rating || 0
}
};
await updateDoc(userDocRef, newProfileData);
// Aggiorna lo stato locale dopo il salvataggio
userProfile = { ...userProfile, ...newProfileData };
userName = getFullName(userProfile); // Aggiorna il nome globale
showModal('Successo!', 'Il tuo profilo è stato aggiornato nel database.');
} catch (error) {
console.error("Errore nel salvataggio del profilo:", error);
showModal('Errore Salvataggio', `Si è verificato un errore: ${error.message}`);
} finally {
hideLoading();
// Ricarica la vista per mostrare i dati aggiornati
changeView('profile');
}
}
// --- Funzioni di Utility ---
window.showModal = (title, message) => {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-message').textContent = message;
document.getElementById('modal').classList.remove('hidden');
document.getElementById('modal').classList.add('flex');
}
window.closeModal = () => {
document.getElementById('modal').classList.add('hidden');
document.getElementById('modal').classList.remove('flex');
}
function showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
function getPublicCollectionPath(collectionName) {
return `artifacts/${appId}/public/data/${collectionName}`;
}
// Helper per costruire l'URL dell'immagine, gestisce sia gli URL che le stringhe Base64
const getImageUrl = (data) => {
// Se la stringa inizia con 'http' è un URL classico
if (data && data.startsWith('http')) {
return data;
}
// Altrimenti, assumiamo che sia una stringa Base64 e creiamo un Data URL
if (data && data.length > 100) {
return `data:image/jpeg;base64,${data}`; // Assumiamo jpeg per Base64 generico
}
return 'https://placehold.co/100x100/cccccc/white?text=Foto'; // Placeholder
}
// --- Logica Galleria ---
window.openGallery = (imageUrlsJson, startIndex) => {
try {
currentImages = JSON.parse(imageUrlsJson);
currentIndex = parseInt(startIndex);
if (currentImages.length === 0) {
showModal('Nessun Allegato', 'Non ci sono immagini caricate per questo lavoro.');
return;
}
showImage(currentIndex);
const galleryModal = document.getElementById('gallery-modal');
galleryModal.classList.remove('hidden');
galleryModal.classList.add('flex');
} catch (e) {
console.error("Errore nell'aprire la galleria:", e);
showModal('Errore Galleria', 'Impossibile caricare le immagini.');
}
};
window.closeGallery = () => {
const galleryModal = document.getElementById('gallery-modal');
galleryModal.classList.add('hidden');
galleryModal.classList.remove('flex');
currentImages = [];
currentIndex = 0;
};
function showImage(index) {
if (index >= 0 && index < currentImages.length) {
currentIndex = index;
const imgEl = document.getElementById('current-gallery-image');
// Utilizza getImageUrl per gestire Base64
imgEl.src = getImageUrl(currentImages[currentIndex]);
document.getElementById('prev-btn').disabled = (currentIndex === 0);
document.getElementById('next-btn').disabled = (currentIndex === currentImages.length - 1);
}
}
window.navigateGallery = (direction) => {
const newIndex = currentIndex + direction;
if (newIndex >= 0 && newIndex < currentImages.length) {
showImage(newIndex);
}
};
// --- Gestione Viste ---
window.changeView = (view) => {
currentView = view;
const contentArea = document.getElementById('content-area');
const clientBtn = document.getElementById('client-view-btn');
const professionalBtn = document.getElementById('professional-view-btn');
const profileBtn = document.getElementById('profile-view-btn');
clientBtn.classList.remove('bg-indigo-500');
professionalBtn.classList.remove('bg-indigo-500');
profileBtn.classList.remove('bg-indigo-500');
clientBtn.classList.remove('bg-gray-600');
professionalBtn.classList.remove('bg-gray-600');
profileBtn.classList.remove('bg-gray-600');
if (view === 'client') {
clientBtn.classList.add('bg-indigo-500');
professionalBtn.classList.add('bg-gray-600');
profileBtn.classList.add('bg-gray-600');
contentArea.innerHTML = renderClientView();
} else if (view === 'professional') {
professionalBtn.classList.add('bg-indigo-500');
clientBtn.classList.add('bg-gray-600');
profileBtn.classList.add('bg-gray-600');
contentArea.innerHTML = renderProfessionalView();
} else if (view === 'profile') {
profileBtn.classList.add('bg-indigo-500');
clientBtn.classList.add('bg-gray-600');
professionalBtn.classList.add('bg-gray-600');
contentArea.innerHTML = renderProfileManagementView();
}
// Avvia i listeners solo se l'utente è autenticato
if (userId) {
startDataListeners();
}
}
// Funzione per mostrare i nomi dei file selezionati
window.displaySelectedFiles = (input) => {
const preview = document.getElementById('selected-files-preview');
const maxFiles = 5;
const maxFileSize = 400 * 1024; // 400 KB per file, per stare sotto il limite di 1MB totale di Firestore
if (input.files.length > maxFiles) {
showModal('Limite File', `Puoi selezionare un massimo di ${maxFiles} file.`);
input.value = ''; // Resetta l'input
preview.textContent = "Nessun file selezionato.";
return;
}
let totalSize = 0;
let oversizedFiles = [];
Array.from(input.files).forEach(file => {
if (file.size > maxFileSize) {
oversizedFiles.push(file.name);
}
totalSize += file.size;
});
if (oversizedFiles.length > 0) {
showModal('File Troppo Grandi', `I seguenti file superano il limite di ${Math.round(maxFileSize / 1024)}KB e non verranno caricati: ${oversizedFiles.join(', ')}.`);
input.value = '';
preview.textContent = "Nessun file selezionato.";
return;
}
if (input.files.length > 0) {
preview.innerHTML = `<span class="font-semibold">${input.files.length}</span> file selezionati: ` + Array.from(input.files).map(f => f.name).join(', ');
} else {
preview.textContent = "Nessun file selezionato.";
}
};
// --- Logica di Rendering e Formulari ---
/**
* Renders the static content section (Services and Regions).
*/
function renderStaticContent() {
const categoriesHtml = SERVICE_CATEGORIES.map(cat =>
`<li class="p-2 bg-gray-50 rounded-lg text-sm font-medium text-gray-700 transition duration-150 hover:bg-indigo-50">${cat}</li>`
).join('');
const regionsHtml = SERVICE_REGIONS.map(reg => `
<div class="mb-4">
<h4 class="text-lg font-semibold text-indigo-700">${reg.region}</h4>
<p class="text-sm text-gray-600">${reg.cities.join(', ')}</p>
</div>
`).join('');
return `
<div class="mt-12 pt-8 border-t-4 border-indigo-200 space-y-12">
<!-- Sezione Servizi Disponibili -->
<section>
<h3 class="text-3xl font-extrabold text-gray-900 mb-6">Elenco dei Servizi Disponibili 🛠️</h3>
<p class="text-lg text-gray-700 mb-6">Offriamo un ampio catalogo di categorie professionali. Pubblica la tua richiesta e ricevi preventivi mirati in queste aree.</p>
<ul class="grid grid-cols-1 md:grid-cols-3 gap-3 list-none p-0">
${categoriesHtml}
</ul>
</section>
<!-- Sezione Regioni e Città -->
<section>
<h3 class="text-3xl font-extrabold text-gray-900 mb-6">Principali Regioni e Città Incluse 🗺️</h3>
<p class="text-lg text-gray-700 mb-6">I nostri professionisti coprono le aree ad alta densità in Italia, garantendo disponibilità e rapidità.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-4 bg-white rounded-xl border border-gray-200">
${regionsHtml}
</div>
</section>
</div>
`;
}
function renderProfileManagementView() {
const profData = userProfile.professionalProfile || {};
const isProfessional = profData.isProfessional || false;
const specialization = profData.specialization || '';
const bio = profData.bio || '';
// Dati anagrafici esistenti
const firstName = userProfile.firstName || '';
const lastName = userProfile.lastName || '';
const address = userProfile.address || '';
const city = userProfile.city || '';
const zip = userProfile.zip || '';
const phone = userProfile.phone || '';
return `
<h2 class="text-3xl font-extrabold text-gray-900 mb-6 border-b pb-2">Gestione Profilo Utente 👤</h2>
<div class="p-6 bg-white rounded-xl container-shadow mb-8">
<h3 class="text-2xl font-semibold text-indigo-700 mb-4">I tuoi Dati Anagrafici (Visibili ai Professionisti/Clienti Accettati)</h3>
<p class="text-gray-600 mb-6">Completa i tuoi dati per ricevere e inviare preventivi in modo professionale.</p>
<form onsubmit="saveProfessionalProfile(event)" class="space-y-6">
<!-- Sezione Dati Anagrafici -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="first-name" class="block text-sm font-medium text-gray-700 mb-1">Nome</label>
<input type="text" id="first-name" value="${firstName}" placeholder="Nome" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="last-name" class="block text-sm font-medium text-gray-700 mb-1">Cognome</label>
<input type="text" id="last-name" value="${lastName}" placeholder="Cognome" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="city" class="block text-sm font-medium text-gray-700 mb-1">Città</label>
<input type="text" id="city" value="${city}" placeholder="Città" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="zip" class="block text-sm font-medium text-gray-700 mb-1">CAP</label>
<input type="text" id="zip" value="${zip}" pattern="[0-9]{5}" placeholder="CAP (es. 20100)" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">Telefono</label>
<input type="tel" id="phone" value="${phone}" placeholder="Numero di Telefono" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<div>
<label for="address" class="block text-sm font-medium text-gray-700 mb-1">Indirizzo (Via e Numero Civico)</label>
<input type="text" id="address" value="${address}" placeholder="Via / Piazza" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<!-- Separatore per la Sezione Professionale -->
<div class="pt-6 border-t border-gray-200">
<h3 class="text-2xl font-semibold text-indigo-700 mb-4">Profilo Professionista (Se applicabile)</h3>
<div class="flex items-center space-x-3 p-4 bg-gray-50 rounded-lg border mb-4">
<input type="checkbox" id="is-professional-checkbox" ${isProfessional ? 'checked' : ''} class="h-5 w-5 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<label for="is-professional-checkbox" class="text-lg font-medium text-gray-900">Sono un Professionista / Fornitore di Servizi</label>
</div>
<div>
<label for="prof-specialization" class="block text-sm font-medium text-gray-700 mb-1">Specializzazione o Settore (es. Architetto, Sgombero Appartamenti)</label>
<input type="text" id="prof-specialization" value="${specialization}" placeholder="Il tuo campo di competenza principale" ${isProfessional ? 'required' : ''} class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="prof-bio" class="block text-sm font-medium text-gray-700 mb-1">Breve Biografia e Esperienza</label>
<textarea id="prof-bio" placeholder="Descrivi la tua esperienza, i tuoi punti di forza e perché i clienti dovrebbero sceglierti (max 300 caratteri)" rows="4" ${isProfessional ? 'required' : ''} class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">${bio}</textarea>
</div>
</div>
<button type="submit" class="w-full bg-indigo-600 text-white font-bold py-3 rounded-lg hover:bg-indigo-700 transition duration-300 transform hover:scale-[1.01] shadow-md">
Salva e Aggiorna Profilo
</button>
</form>
</div>
`;
}
function renderClientView() {
// Ottieni la data minima (oggi) per il campo data
const today = new Date().toISOString().split('T')[0];
return `
<h2 class="text-3xl font-extrabold text-gray-900 mb-6 border-b pb-2">Sezione Cliente: Pubblica un Lavoro 🚀</h2>
<div class="p-6 bg-white rounded-xl container-shadow mb-8">
<h3 class="text-2xl font-semibold text-indigo-700 mb-4">1. Crea un annuncio (Ispirazione LoveJunk)</h3>
<p class="text-gray-600 mb-6">Descrivi il servizio professionale di cui hai bisogno. I professionisti verificati invieranno i loro preventivi.</p>
<form id="job-post-form" class="space-y-4">
<input type="text" id="job-title" placeholder="Titolo del Lavoro (es. Sgombero Cantina a Milano)" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
<textarea id="job-description" placeholder="Descrizione dettagliata del progetto e dei requisiti (dimensioni, tipo di merce, piano)..." rows="4" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"></textarea>
<!-- Prenotazione Data e Ora -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="preferred-date" class="block text-sm font-medium text-gray-700 mb-1">Data di preferenza per l'inizio del servizio</label>
<input type="date" id="preferred-date" min="${today}" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="preferred-time" class="block text-sm font-medium text-gray-700 mb-1">Fascia oraria preferita</label>
<select id="preferred-time" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 bg-white">
<option value="" disabled selected>Seleziona fascia oraria</option>
<option value="Mattina (8:00 - 12:00)">Mattina (8:00 - 12:00)</option>
<option value="Pomeriggio (12:00 - 17:00)">Pomeriggio (12:00 - 17:00)</option>
<option value="Sera (17:00 - 21:00)">Sera (17:00 - 21:00)</option>
<option value="Flessibile">Flessibile</option>
</select>
</div>
</div>
<!-- Fine Prenotazione Data e Ora -->
<!-- Nuovo Bottone/Campo per l'allegato file -->
<div class="space-y-3 p-4 bg-gray-50 rounded-lg border">
<input type="file" id="job-files" multiple accept="image/*" class="hidden" onchange="displaySelectedFiles(this)">
<button type="button" onclick="document.getElementById('job-files').click()" class="flex items-center space-x-2 bg-gray-200 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-300 transition duration-300 shadow-sm">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
</svg>
<span class="font-semibold">Carica Foto/Allegati (Max 5 file)</span>
</button>
<div id="selected-files-preview" class="text-sm text-gray-600">Nessun file selezionato.</div>
<p class="text-xs text-red-500 mt-1">
ATTENZIONE: Le immagini vengono salvate direttamente nel database come Base64. Limita le dimensioni e il numero dei file per non superare il limite di 1MB per richiesta.
</p>
</div>
<button type="submit" class="w-full bg-green-500 text-white font-bold py-3 rounded-lg hover:bg-green-600 transition duration-300 transform hover:scale-[1.01] shadow-md">Pubblica Richiesta</button>
</form>
</div>
<h3 class="text-3xl font-extrabold text-gray-900 mb-6 border-b pb-2 mt-10">I tuoi Lavori Pubblicati</h3>
<div id="client-jobs-list" class="space-y-4">
<p class="text-gray-500">Nessun lavoro pubblicato o caricamento in corso...</p>
</div>
${renderStaticContent()}
`;
}
function renderProfessionalView() {
return `
<h2 class="text-3xl font-extrabold text-gray-900 mb-6 border-b pb-2">Sezione Professionista: Trova Lavori 💼</h2>
<div class="p-6 bg-white rounded-xl container-shadow mb-8">
<h3 class="text-2xl font-semibold text-indigo-700 mb-4">2. Visualizza Lavori e Invia Preventivi</h3>
<p class="text-gray-600">Compara prezzi e recensioni! Scegli il lavoro migliore per te e fai la tua offerta.</p>
${userProfile.professionalProfile?.isProfessional ?
`<div class="p-3 bg-green-50 text-green-700 rounded-lg">Il tuo account è impostato come **Professionista**. Puoi inviare preventivi.</div>` :
`<div class="p-3 bg-red-100 text-red-700 rounded-lg">Attenzione! Vai su **Gestisci Profilo** per attivare il tuo account come Professionista.</div>`
}
</div>
<div id="professional-jobs-list" class="space-y-4">
<p class="text-gray-500">Nessun lavoro disponibile o caricamento in corso...</p>
</div>
${renderStaticContent()}
`;
}
// --- Logica CRUD Firebase ---
let unsubscribeJobs = null;
let jobList = [];
function startDataListeners() {
if (unsubscribeJobs) {
unsubscribeJobs();
}
const jobsColRef = collection(db, getPublicCollectionPath('jobs'));
const jobsQuery = query(jobsColRef, orderBy("createdAt", "desc"));
// Listener principale sui lavori
unsubscribeJobs = onSnapshot(jobsQuery, async (snapshot) => {
showLoading();
jobList = [];
const jobPromises = snapshot.docs.map(async (jobDoc) => {
const job = { id: jobDoc.id, ...jobDoc.data() };
// Ottieni i preventivi per ogni lavoro
const bidsColRef = collection(db, getPublicCollectionPath(`jobs/${job.id}/bids`));
const bidsSnapshot = await getDocs(query(bidsColRef));
job.bids = bidsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
return job;
});
// Aggiungiamo un fallback per 'createdAt' se serverTimestamp non è ancora risolto
jobList = (await Promise.all(jobPromises)).sort((a, b) => {
const timeA = a.createdAt?.seconds || 0;
const timeB = b.createdAt?.seconds || 0;
return timeB - timeA;
});
// Re-rendering delle liste
if (currentView === 'client') {
renderClientJobs(jobList);
} else if (currentView === 'professional') {
renderProfessionalJobs(jobList);
}
// Non renderizziamo qui la vista 'profile' che si aggiorna solo al salvataggio
hideLoading();
}, (error) => {
console.error("Errore nel listener dei lavori:", error);
hideLoading();
});
}
/**
* Converte un oggetto File in una stringa Base64 (solo la parte dei dati).
* @param {File} file - Il file da leggere.
* @returns {Promise<string>} Promessa che risolve con la stringa Base64.
*/
const readFileAsBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Estrae solo la parte Base64 dopo il Data URL prefix (es. "data:image/jpeg;base64,")
const base64String = reader.result.split(',')[1];
resolve(base64String);
};
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file);
});
};
// Funzione per l'aggiunta di un nuovo lavoro
window.handleJobPost = async (event) => {
event.preventDefault();
if (!userId) {
showModal('Autenticazione Richiesta', 'Attendi il completamento dell'autenticazione per pubblicare un lavoro.');
return;
}
const title = document.getElementById('job-title').value;
const description = document.getElementById('job-description').value;
const preferredDate = document.getElementById('preferred-date').value;
const preferredTime = document.getElementById('preferred-time').value;
const fileInput = document.getElementById('job-files');
const files = fileInput.files;
let base64ImageStrings = [];
// Re-validazione dei limiti di dimensione e conteggio dei file (per sicurezza)
const maxFiles = 5;
// Impostiamo il limite a 400KB (anziché 1MB) per file per lasciare spazio al resto del documento Firestore (max 1MB totale)
const maxFileSize = 400 * 1024;
if (files.length > maxFiles) {
showModal('Limite File', `Puoi selezionare un massimo di ${maxFiles} file. Riprova con meno file.`);
return;
}
if (files.length > 0) {
showLoading(); // Mostra il caricamento durante la conversione
try {
const conversionPromises = Array.from(files).map(file => {
if (file.size > maxFileSize) {
throw new Error(`Il file ${file.name} è troppo grande (${Math.round(file.size / 1024)} KB). Massimo consentito: ${Math.round(maxFileSize / 1024)} KB.`);
}
return readFileAsBase64(file);
});
base64ImageStrings = await Promise.all(conversionPromises);
} catch (error) {
console.error("Errore nella conversione Base64 o dimensione eccessiva:", error);
hideLoading();
showModal('Errore di Caricamento', `Impossibile leggere i file selezionati: ${error.message}. Riduci le dimensioni delle immagini.`);
return;
}
}
showLoading();
try {
const jobsColRef = collection(db, getPublicCollectionPath('jobs'));
await addDoc(jobsColRef, {
title: title,
description: description,
preferredDate: preferredDate,
preferredTime: preferredTime,
// Il campo imageUrls ora contiene Base64 (se caricato) o URL (se usiamo i placeholder)
imageUrls: base64ImageStrings,
status: 'Open',
clientId: userId,
clientName: userName,
selectedBidId: null,
acceptedAmount: null,
professionalId: null,
createdAt: serverTimestamp()
});
document.getElementById('job-post-form').reset();
document.getElementById('selected-files-preview').textContent = "Nessun file selezionato."; // Reset la preview
showModal('Successo!', 'La tua richiesta è stata pubblicata. I professionisti inizieranno a inviare i preventivi.');
} catch (error) {
console.error("Errore nella pubblicazione del lavoro:", error);
// Miglioriamo la gestione dell'errore per dare un feedback più utile
let errorMessage = `Si è verificato un errore: ${error.message}`;
if (error.code && error.code === 'invalid-argument') {
errorMessage = "Errore: La richiesta contiene un argomento non valido. Questo può accadere se il documento, inclusi gli allegati, supera il limite di 1MB di Firestore. Riprova con immagini più piccole o meno numerose.";
}
showModal('Errore di Pubblicazione', errorMessage);
} finally {
hideLoading();
}
}
// Funzione per l'aggiunta di un preventivo
window.handleBidSubmit = async (jobId) => {
if (!userProfile.professionalProfile?.isProfessional) {
showModal('Azione non consentita', 'Devi attivare il tuo account come **Professionista** in "Gestisci Profilo" prima di inviare preventivi.');
return;
}
if (!userProfile.firstName || !userProfile.phone) {
showModal('Profilo Incompleto', 'Devi completare i tuoi dati anagrafici (Nome, Cognome, Telefono) in "Gestisci Profilo" per inviare un preventivo.');
return;
}
const bidAmountInput = document.getElementById(`bid-amount-${jobId}`);
const bidMessageInput = document.getElementById(`bid-message-${jobId}`);
const amount = parseFloat(bidAmountInput.value);
const message = bidMessageInput.value;
if (isNaN(amount) || amount <= 0) {
showModal('Errore di Input', 'L'importo del preventivo deve essere un numero positivo.');
return;
}
showLoading();
try {
const bidsColRef = collection(db, getPublicCollectionPath(`jobs/${jobId}/bids`));
// Controlla se il professionista ha già inviato un'offerta
const existingBidsQuery = query(bidsColRef, where("professionalId", "==", userId));
const existingBidsSnapshot = await getDocs(existingBidsQuery);
if (!existingBidsSnapshot.empty) {
showModal('Preventivo già inviato', 'Hai già inviato un preventivo per questo lavoro. Attendi la risposta del cliente.');
return;
}
const profSpecialization = userProfile.professionalProfile?.specialization || 'Generico';
const profBio = userProfile.professionalProfile?.bio || 'Nessuna biografia fornita';
await addDoc(bidsColRef, {
professionalId: userId,
professionalName: userName,
professionalSpecialization: profSpecialization,
professionalBio: profBio,
amount: amount,
message: message,
createdAt: serverTimestamp()
});
showModal('Preventivo Inviato!', 'La tua offerta è stata inviata al cliente. Riceverai una notifica se verrà accettata.');
bidAmountInput.value = '';
bidMessageInput.value = '';
} catch (error) {
console.error("Errore nell'invio del preventivo:", error);
showModal('Errore Preventivo', `Si è verificato un errore: ${error.message}`);
} finally {
hideLoading();
}
}
// Funzione per l'accettazione di un preventivo (LoveJunk step 2: Choose Best Quote)
window.acceptBid = async (jobId, bidId, professionalId, amount) => {
showLoading();
try {
const jobDocRef = doc(db, getPublicCollectionPath('jobs'), jobId);
await updateDoc(jobDocRef, {
status: 'Accepted',
selectedBidId: bidId,
acceptedAmount: amount,
professionalId: professionalId,
});
showModal('Preventivo Accettato!', `Hai accettato l'offerta di ${userName}. Il pagamento di €${amount} è ora in Escrow e verrà rilasciato al completamento. Contattalo tramite la chat in-app (non simulata qui) per i dettagli.`);
} catch (error) {
console.error("Errore nell'accettazione del preventivo:", error);
showModal('Errore Accettazione', `Si è verificato un errore: ${error.message}`);
} finally {
hideLoading();
}
}
// Funzione per segnare come completato (LoveJunk step 3: Pay After Removal/Completion)
window.completeJob = async (jobId) => {
showLoading();
try {
const jobDocRef = doc(db, getPublicCollectionPath('jobs'), jobId);
await updateDoc(jobDocRef, {
status: 'Completed',
completedAt: serverTimestamp()
});
showModal('Lavoro Completato!', 'Il lavoro è stato segnato come completato. Il pagamento è stato rilasciato al professionista. Non dimenticare di lasciare una recensione per la sua verifica futura!');
} catch (error) {
console.error("Errore nel completamento del lavoro:", error);
showModal('Errore Completamento', `Si è verificato un errore: ${error.message}`);
} finally {
hideLoading();
}
}
// --- Rendering Specifico per Vista ---
function renderClientJobs(jobs) {
const listEl = document.getElementById('client-jobs-list');
if (!listEl) return;
const userJobs = jobs.filter(job => job.clientId === userId);
if (userJobs.length === 0) {
listEl.innerHTML = '<p class="text-gray-500 p-4 bg-white rounded-lg container-shadow">Non hai ancora pubblicato lavori. Usa il modulo sopra per iniziare!</p>';
return;
}
listEl.innerHTML = userJobs.map(job => {
const bidCount = job.bids ? job.bids.length : 0;
let statusColor = 'bg-yellow-100 text-yellow-800';
let actionHtml = '';
const bidsList = job.bids.sort((a, b) => a.amount - b.amount); // Ordina per offerta più bassa
// Dettagli Data/Ora
const dateTimeHtml = `
<div class="flex space-x-4 text-sm text-gray-600 mt-2">
<p><span class="font-semibold text-indigo-700">Data Preferita:</span> ${job.preferredDate || 'N/D'}</p>
<p><span class="font-semibold text-indigo-700">Orario:</span> ${job.preferredTime || 'N/D'}</p>
</div>
`;
// Image Gallery HTML
const galleryHtml = (job.imageUrls && job.imageUrls.length > 0) ? `
<div class="mt-4 border-t pt-4 border-gray-100">
<h5 class="text-sm font-semibold text-gray-700 mb-2">Foto Allegati (Clicca per ingrandire):</h5>
<div class="flex flex-wrap gap-3">
${job.imageUrls.map((data, index) => {
const dataUrl = getImageUrl(data); // Usa helper per Base64/URL
const imageUrlsJson = JSON.stringify(job.imageUrls);
const escapedImageUrls = imageUrlsJson.replace(/'/g, "\'");
return `
<img
src="${dataUrl}"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/cccccc/white?text=Foto';"
onclick="openGallery('${escapedImageUrls}', ${index})"
class="w-24 h-24 object-cover rounded-lg border-2 border-gray-200 cursor-pointer transition transform hover:scale-105"
title="Clicca per ingrandire">
`;
}).join('')}
</div>
</div>
` : '';
let acceptedAmountDisplay = '';
if (job.status === 'Open') {
statusColor = 'bg-yellow-100 text-yellow-800';
if (bidCount > 0) {
actionHtml = `
<h4 class="font-semibold text-sm mt-3 mb-2 text-indigo-700">Preventivi Ricevuti (${bidCount}):</h4>
<div class="space-y-2 max-h-48 overflow-y-auto pr-2">
${bidsList.map(bid => `
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg border">
<div>
<p class="font-bold text-lg text-green-700">€${bid.amount.toFixed(2)}</p>
<p class="text-xs text-gray-600">Da: ${bid.professionalName} (${bid.professionalSpecialization || 'Specializzazione Sconosciuta'})</p>
<p class="text-xs text-gray-500 italic mt-1">"${bid.message}"</p>
</div>
<button onclick="acceptBid('${job.id}', '${bid.id}', '${bid.professionalId}', ${bid.amount})" class="bg-indigo-600 text-white text-sm py-1 px-3 rounded-lg hover:bg-indigo-700 transition duration-300 transform hover:scale-[1.05]">
Accetta & Metti in Escrow
</button>
</div>
`).join('')}
</div>
`;
} else {
actionHtml = '<p class="text-gray-500 mt-3">Nessun preventivo ricevuto ancora. Sii paziente!</p>';
}
} else if (job.status === 'Accepted') {
statusColor = 'bg-blue-100 text-blue-800';
// Accepted amount and professional info
const acceptedBid = bidsList.find(b => b.id === job.selectedBidId) || {};
acceptedAmountDisplay = `
<div class="my-3 p-3 bg-indigo-50 rounded-lg border border-indigo-200">
<p class="text-2xl font-bold text-indigo-700">Cifra Accettata: €${job.acceptedAmount ? job.acceptedAmount.toFixed(2) : 'N/D'}</p>
<p class="text-sm text-gray-600 mt-1">Professionista Selezionato: ${acceptedBid.professionalName || 'Sconosciuto'}</p>
<p class="text-xs text-gray-500">Specializzazione: ${acceptedBid.professionalSpecialization || 'N/D'}</p>
</div>
`;
actionHtml = `
<p class="text-green-600 font-semibold mt-3">Preventivo Accettato, Lavoro in Corso.</p>
<button onclick="completeJob('${job.id}')" class="mt-3 bg-green-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-green-700 transition duration-300 transform hover:scale-[1.01]">
3. Conferma Completamento e Rilascia Pagamento
</button>
`;
} else if (job.status === 'Completed') {
statusColor = 'bg-green-100 text-green-800';
// Accepted amount and professional info for Completed
const acceptedBid = bidsList.find(b => b.id === job.selectedBidId) || {};
acceptedAmountDisplay = `
<div class="my-3 p-3 bg-green-50 rounded-lg border border-green-200">
<p class="text-2xl font-bold text-green-700">Cifra Finale Pagata: €${job.acceptedAmount ? job.acceptedAmount.toFixed(2) : 'N/D'}</p>
<p class="text-sm text-gray-600 mt-1">Professionista: ${acceptedBid.professionalName || 'Sconosciuto'}</p>
<p class="text-xs text-gray-500">Specializzazione: ${acceptedBid.professionalSpecialization || 'N/D'}</p>
</div>
`;
actionHtml = '<p class="text-green-600 font-semibold mt-3">Lavoro Completato! Pagamento Rilasciato.</p>';
}
return `
<div class="bg-white p-6 rounded-xl container-shadow border border-gray-200">
<div class="flex justify-between items-start">
<h4 class="text-xl font-bold text-gray-800">${job.title}</h4>
<span class="px-3 py-1 text-sm font-semibold rounded-full ${statusColor}">${job.status === 'Open' ? 'Aperto' : job.status === 'Accepted' ? 'Accettato' : 'Completato'}</span>
</div>
${dateTimeHtml}
<p class="text-gray-700 mt-3">${job.description}</p>
<!-- Accepted Amount and Professional Info -->
${acceptedAmountDisplay}
<!-- Image Gallery -->
${galleryHtml}
<div class="mt-4 pt-4 border-t border-gray-100">
${actionHtml}
</div>
</div>
`;
}).join('');
}
function renderProfessionalJobs(jobs) {
const listEl = document.getElementById('professional-jobs-list');
if (!listEl) return;
// Permetti di inviare preventivi solo se l'utente è un professionista
if (!userProfile.professionalProfile?.isProfessional) {
listEl.innerHTML = '<p class="text-red-500 p-4 bg-white rounded-lg container-shadow">Non puoi inviare preventivi finché non attivi il tuo profilo professionale.</p>';
return;
}
const openJobs = jobs.filter(job => job.status === 'Open');
if (openJobs.length === 0) {
listEl.innerHTML = '<p class="text-gray-500 p-4 bg-white rounded-lg container-shadow">Nessun lavoro aperto disponibile in questo momento.</p>';
return;
}
listEl.innerHTML = openJobs.map(job => {
const userBid = job.bids.find(bid => bid.professionalId === userId);
let bidHtml = '';
// Dettagli Data/Ora
const dateTimeHtml = `
<div class="flex space-x-4 text-sm text-gray-600 mt-2">
<p><span class="font-semibold text-indigo-700">Data Preferita:</span> ${job.preferredDate || 'N/D'}</p>
<p><span class="font-semibold text-indigo-700">Orario:</span> ${job.preferredTime || 'N/D'}</p>
</div>
`;
// Image Gallery HTML (anche per i professionisti)
const galleryHtml = (job.imageUrls && job.imageUrls.length > 0) ? `
<div class="mt-4 pt-4 border-t border-gray-100">
<h5 class="text-sm font-semibold text-gray-700 mb-2">Foto Allegati (Clicca per ingrandire):</h5>
<div class="flex flex-wrap gap-3">
${job.imageUrls.map((data, index) => {
const dataUrl = getImageUrl(data); // Usa helper per Base64/URL
const imageUrlsJson = JSON.stringify(job.imageUrls);
const escapedImageUrls = imageUrlsJson.replace(/'/g, "\'");
return `
<img
src="${dataUrl}"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/cccccc/white?text=Foto';"
onclick="openGallery('${escapedImageUrls}', ${index})"
class="w-20 h-20 object-cover rounded-lg border-2 border-gray-200 cursor-pointer transition transform hover:scale-105"
title="Clicca per ingrandire">
`;
}).join('')}
</div>
</div>
` : '';
if (userBid) {
bidHtml = `
<div class="p-3 bg-indigo-50 rounded-lg border border-indigo-200 mt-3">
<p class="font-semibold text-indigo-700">Il tuo Preventivo Inviato:</p>
<p class="text-xl font-bold text-green-700 mt-1">€${userBid.amount.toFixed(2)}</p>
<p class="text-sm text-gray-600 italic">"${userBid.message}"</p>
<p class="text-xs text-indigo-500 mt-2">In attesa di accettazione del cliente...</p>
</div>
`;
} else {
bidHtml = `
<h4 class="font-semibold text-sm mt-3 mb-2 text-indigo-700">2. Invia il Tuo Preventivo:</h4>
<form onsubmit="event.preventDefault(); handleBidSubmit('${job.id}')" class="space-y-3">
<input type="number" id="bid-amount-${job.id}" placeholder="Il tuo Prezzo (es. 1200)" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 text-lg font-bold">
<textarea id="bid-message-${job.id}" placeholder="Breve messaggio e tempi di consegna (LoveJunk: 'Messaggistica in-app')" rows="2" required class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 text-sm"></textarea>
<button type="submit" class="w-full bg-indigo-600 text-white font-bold py-2 rounded-lg hover:bg-indigo-700 transition duration-300">
Invia Offerta
</button>
</form>
`;
}
return `
<div class="bg-white p-6 rounded-xl container-shadow border border-indigo-100">
<div class="flex justify-between items-center">
<h4 class="text-xl font-bold text-gray-800">${job.title}</h4>
<span class="px-3 py-1 text-sm font-semibold rounded-full bg-gray-200 text-gray-800">Cliente: ${job.clientName}</span>
</div>
${dateTimeHtml}
<p class="text-gray-700 mt-3">${job.description}</p>
${galleryHtml}
${bidHtml}
</div>
`;
}).join('');
}
// --- Altre Funzioni (Inizializzazione) ---
// Inizializzazione
window.onload = () => {
initializeFirebase();
// Aggiunge il listener per il form solo dopo che il DOM è pronto
const jobForm = document.getElementById('job-post-form');
if (jobForm) {
jobForm.addEventListener('submit', handleJobPost);
}
};
// Assicura che l'event listener per il form sia sempre disponibile
document.addEventListener('DOMContentLoaded', () => {
const contentArea = document.getElementById('content-area');
const observer = new MutationObserver(() => {
const jobForm = document.getElementById('job-post-form');
if (jobForm && !jobForm.__listenerAdded) {
jobForm.addEventListener('submit', handleJobPost);
jobForm.__listenerAdded = true; // Flag per evitare duplicati
}
});
observer.observe(contentArea, { childList: true, subtree: true });
});
</script>
</body>
</html>

