File: /var/www/html/resources/views/livewire/buyer/chat.blade.php
<div>
<script type="module">
import {
initializeApp,
} from "https://www.gstatic.com/firebasejs/9.17.2/firebase-app.js";
import {
getFirestore,
collection,
query,
where,
onSnapshot,
doc,
} from "https://www.gstatic.com/firebasejs/9.17.2/firebase-firestore.js";
import {
getMessaging
} from "https://www.gstatic.com/firebasejs/11.1.0/firebase-messaging.js";
// Firebase Configuration
const firebaseConfig = {
apiKey: "AIzaSyBttDksOxuahV-SCex9ho2SpHrYeSbwnj4",
authDomain: "fixgini.firebaseapp.com",
projectId: "fixgini",
storageBucket: "fixgini.appspot.com",
messagingSenderId: "994362424638",
appId: "1:994362424638:web:9b0a92ba6c61ecabd6006f",
measurementId: "G-ECEEH8CQVH",
};
// Initialize Firebase and Firestore
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const authenticatedUserUID = "{{$user_id}}"; // Pass this dynamically from the backend
window.renderMessages = renderMessages;
// Update renderMessages function to filter chats involving the authenticated user
async function renderMessages() {
try {
const chatsRef = collection(db, "chats");
const q = query(chatsRef, where("participantsUIDs", "array-contains", authenticatedUserUID));
onSnapshot(q, (snapshot) => {
const allChats = [];
const engagedChats = [];
const completedChats = [];
snapshot.forEach((chatDoc) => {
const chatData = chatDoc.data();
const chatRoomStatus = chatData.chat_room_status;
const participantsUIDs = chatData.participantsUIDs;
// Ensure the authenticated user is part of the chat
if (participantsUIDs.includes(authenticatedUserUID)) {
if (chatRoomStatus === "active") {
engagedChats.push(chatData);
} else if (chatRoomStatus === "completed") {
completedChats.push(chatData);
} else {
allChats.push(chatData);
}
}
});
// Render All and Engaged chats dynamically
renderChatList("all", allChats);
renderChatList("engaged", engagedChats);
renderChatList("completed", completedChats);
});
} catch (error) {
console.error("Error rendering messages:", error);
}
}
// Function to render chat list
function renderChatList(tab, chats) {
const chatListContainer = document.getElementById(tab);
chatListContainer.innerHTML = ""; // Clear old chat list
const groupedChats = {};
chats.sort((a, b) => new Date(b.lastMessageAt) - new Date(a.lastMessageAt));
chats.forEach((chat) => {
const participantsUIDs = chat.participantsUIDs;
// Find the other participant (not the authenticated user)
const otherParticipantUID = participantsUIDs.find(uid => uid !== authenticatedUserUID);
if (!otherParticipantUID) {
console.warn("No other participant found for chat:", chat.chatId);
return;
}
const otherParticipant = chat.participants.find(p => p.userUID === otherParticipantUID);
if (otherParticipant) {
const participantPhoto = otherParticipant.userPhoto || "https://console.fixgini.com/icon.png";
const participantName = otherParticipant.firstName || "Unknown";
const lastMessage = chat.lastMessage || "No messages";
const lastMessageTruncated = lastMessage.length > 25 ? lastMessage.substring(0, 25) + '...' : lastMessage;
// Group chats by participants
if (!groupedChats[otherParticipantUID] || new Date(chat.lastMessageAt) > new Date(groupedChats[otherParticipantUID].lastMessageAt)) {
groupedChats[otherParticipantUID] = {
photo: participantPhoto,
name: participantName,
lastMessage: lastMessageTruncated,
chatId: chat.chatId,
};
}
}
});
// Render the grouped chats
if (Object.keys(groupedChats).length === 0) {
const noChatMessage = document.createElement("div");
noChatMessage.className = "no-chat-message text-center mt-1";
noChatMessage.innerText = "No chat contact";
chatListContainer.appendChild(noChatMessage);
} else {
Object.values(groupedChats).forEach(chat => {
console.log('incoming new chat');
const chatId = sessionStorage.getItem('chatId');
const photo = sessionStorage.getItem('photo');
const name = sessionStorage.getItem('name');
// Call the loadChatDetails function and wait for it to complete
loadChatDetails(chatId, photo, name);
window.renderMessages = renderMessages;
// can i use livewire refresh approach here
Swal.fire({
text: 'You have a new chat message.',
position: 'top-end', // Positioning at the top-right
showConfirmButton: false, // To show the confirm button
timer: 6000, // Auto close after 3 seconds
timerProgressBar: true, // Show a progress bar during the timer countdown
width: '300px', // Adjust the width to make it smaller
padding: '10px', // Reduce padding to make it more compact
customClass: {
popup: 'small-popup' // Custom class for further styling if needed
}
});
// Optionally add custom CSS for better styling
const style = document.createElement('style');
style.innerHTML = `
.small-popup {
font-size: 16px;
padding: 10px;
}
`;
const chatMemberElement = document.createElement("div");
chatMemberElement.className = "chat-member d-flex align-items-center mb-3";
chatMemberElement.style.borderBottom = "1px solid #cc6"; // Add bottom border
chatMemberElement.innerHTML = `
<img src="${chat.photo}" class="rounded-circle ml-3 mb-2" width="50" height="50" />
<div class="ml-3">
<h6 class="mb-0">${chat.name}</h6>
<p class="mb-0 text-muted small">${chat.lastMessage}</p>
</div>
`;
chatMemberElement.addEventListener('click', async function() {
await loadChatDetails(chat.chatId, chat.photo, chat.name);
});
chatListContainer.appendChild(chatMemberElement);
});
}
}
async function loadChatDetails(chatId, photo, name) {
console.log('loading new chat');
const audio = new Audio('https://fixgini.com/notification.wav'); // Replace with your sound file URL
// Play the audio
audio.play().catch((error) => {
console.error("Error playing the notification sound: ", error);
});
sessionStorage.setItem('chatId', chatId);
sessionStorage.setItem('photo', photo);
sessionStorage.setItem('name', name);
try {
const userHeading = document.getElementById("user-heading");
if (name == null) {
userHeading.innerHTML = ''; // Clear the content if name or photo is null/empty
} else {
userHeading.innerHTML = `
<img src="${photo}" class="rounded-circle" width="50" height="50" />
<div class="ml-3">
<h6 class="mb-0">${name}</h6>
<span class="text-success small">Online</span>
</div>
`;
}
const chatMessagesList = document.getElementById("chat-messages-list");
chatMessagesList.innerHTML = "";
const chatMessages = await fetchChatRoomDetails(chatId);
let showActionButton = false;
let showCompleteButton = false;
let paymentLink = null;
let amountToPay = '';
let gigCurrency = '';
let gigName = '';
let gigId = '';
let bookingId = '';
if (!chatMessages || chatMessages.length === 0) {
if (name == null) {
chatMessagesList.innerHTML = `<p class="text-muted text-center">No messages yet</p>`;
} else {
chatMessagesList.innerHTML = `<p class="text-muted text-center">Select chat to see conversation</p>`;
}
} else {
chatMessages.sort((a, b) => new Date(a.time) - new Date(b.time));
chatMessages.forEach(chatMessage => {
const chatDate = new Date(chatMessage.time);
const currentDate = new Date();
const authUserId = "{{$user_id}}";
// Check if action button should be shown to customer or provider, so it only proivder to acept it should show to
if (chatMessage.customerId != authUserId) {
if (chatMessage.showActionButton) {
showActionButton = true;
amountToPay = chatMessage.amount || ''; // Assign the value from chatMessage
gigCurrency = chatMessage.gigCurrency || ''; // Assign the value from chatMessage
gigName = chatMessage.gigName || ''; // Assign the value from chatMessage
gigId = chatMessage.gigId || ''; // Assign the value from chatMessage
bookingId = chatMessage.bookingId || ''; // Assign the value from chatMessage
}
}
// only customer should see the payment link
if (chatMessage.customerId === authUserId) {
if (chatMessage.paymentLink) {
paymentLink = chatMessage.paymentLink; // Update the payment link if found
}
}
// only customer should see the completion button
if (chatMessage.customerId === authUserId) {
if (chatMessage.showCompleteButton) {
showCompleteButton = true;
gigName = chatMessage.gigName || ''; // Assign the value from chatMessage
gigId = chatMessage.gigId || ''; // Assign the value from chatMessage
bookingId = chatMessage.bookingId || ''; // Assign the value from chatMessage
}
}
// Function to format the date as requested
function formatDate(date) {
const today = new Date();
const dayOfWeek = date.toLocaleString('en-US', {
weekday: 'short'
});
const options = {
hour: '2-digit',
minute: '2-digit',
hour12: true
};
// Check if the date is today
if (date.toDateString() === today.toDateString()) {
return date.toLocaleString('en-US', options); // Just show time like 04:12 PM
}
// Check if the date is within the same week
const diffInDays = Math.floor((today - date) / (1000 * 60 * 60 * 24)); // Difference in days
if (diffInDays < 7) {
return `${dayOfWeek} ${date.toLocaleString('en-US', options)}`; // Show day and time like Thurs 04:56 PM
}
// If the date is older than a week
return date.toLocaleString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}); // Show date and time like 16/04/2025 06:56 AM
}
const formattedTime = formatDate(chatDate);
const messageElement = document.createElement("div");
const isSender = chatMessage.sentBy === "{{$user_id}}"; // Compare the sender with the auth user ID
messageElement.className = `message mb-3 d-flex justify-content-${isSender ? "end" : "start"}`;
messageElement.innerHTML = `
<div class="p-2 ${isSender ? "text-primary btn-light-thm rounded-end" : "text-white bg-primary rounded-start"}" style="border-radius: 25px">
${chatMessage.message || "No content"}
<small class="d-block mt-1 text-end ${isSender ? "text-primary" : "text-white"}" style="font-size: 12px;">
${formattedTime}
</small>
</div>
`;
chatMessagesList.appendChild(messageElement);
});
// Update the UI for action button and payment link
const actionButtonContainer = document.getElementById("action-button-container");
const paymentLinkContainer = document.getElementById("payment-link-container");
const showCompleteButtonContainer = document.getElementById("show-complete-button-container");
if (showCompleteButton) {
showCompleteButtonContainer.innerHTML = `
<p>Provider await your confirmation for completion - <strong>${gigName || ''}</strong> </p>
<button id="confirm-button" class="btn btn-success text-white mr-2 fw-bold">
Confirm Completion
</button>
`;
// Attach event listeners after the buttons are added to the DOM
document.getElementById("confirm-button").addEventListener("click", function() {
handleConfirmAction(bookingId);
});
} else {
showCompleteButtonContainer.innerHTML = `
`;
}
if (showActionButton) {
actionButtonContainer.innerHTML = `
<p>Customer wants to engage your <strong>${gigName}, for ${gigCurrency} ${amountToPay || 'N/A'}</strong> </p>
<button id="accept-button" class="btn btn-success text-white mr-2 fw-bold">
Accept
</button>
<button id="decline-button" class="btn btn-danger text-white fw-bold">
Decline
</button>
`;
// Attach event listeners after the buttons are added to the DOM
document.getElementById("accept-button").addEventListener("click", function() {
handleAcceptAction(gigId, bookingId);
});
document.getElementById("decline-button").addEventListener("click", function() {
handleDeclineAction(gigId, bookingId);
});
} else {
actionButtonContainer.innerHTML = `
`;
}
if (paymentLink) {
paymentLinkContainer.innerHTML = `
<a href="${paymentLink}" target="_blank" class="btn btn-light-thm text-primary">
Make Payment
</a>
`;
} else {
paymentLinkContainer.innerHTML = `
`;
}
}
} catch (error) {
console.error("Error loading chat details:", error);
}
}
async function fetchChatRoomDetails(chatId) {
try {
sessionStorage.setItem('chatId', chatId);
const response = await fetch(`https://firestore.googleapis.com/v1/projects/fixgini/databases/(default)/documents/chats/${chatId}/chat_room`);
const data = await response.json();
// Extract chat room details
if (data.documents) {
const chatMessages = data.documents.map(doc => {
try {
// Extract values from fields
const fields = doc.fields || {};
const sentBy = fields.sentBy?.stringValue || null;
const message = fields.message?.stringValue || "";
const time = fields.time?.stringValue || null;
const paymentLink = fields.paymentLink?.stringValue || null;
// Extract participants array to get customer and provider IDs
const participants = fields.participants?.arrayValue?.values || [];
const customerId = participants[0]?.stringValue || null; // First value is customer
const providerId = participants[1]?.stringValue || null; // Second value is provider
// Extract chatHire or webHire details
// Extract chatHire or webHire details
const hireDetails = fields.webHire?.mapValue?.fields || fields.chatHire?.mapValue?.fields || {};
const accepted = hireDetails.accepted?.booleanValue ?? null; // Ensure to access booleanValue
const confirmCompleted = hireDetails.confirmCompleted?.booleanValue ?? null; // Ensure to access booleanValue
const completed = hireDetails.completed?.booleanValue ?? null; // Ensure to access booleanValue
const paid = hireDetails.paid?.booleanValue ?? null; // Ensure to access booleanValue
const amount = hireDetails.price?.doubleValue ?? '';
const gigCurrency = hireDetails.gigCurrency?.stringValue ?? '';
const gigName = hireDetails.gigName?.stringValue ?? '';
const gigId = hireDetails.gigId?.stringValue ?? '';
const bookingId = hireDetails.bookingId?.stringValue ?? '';
// Set the bookingId session
if (bookingId) {
sessionStorage.setItem('bookingId', bookingId);
}
// Determine if the action button should be shown
const showActionButton = accepted === false && paid === false;
const showCompleteButton = completed === true && confirmCompleted === false;
// const showActionButton = accepted === false && paid === false && paymentLink === null;
return {
sentBy,
amount,
gigCurrency,
gigName,
gigId,
bookingId,
customerId,
providerId,
message,
time,
paymentLink,
showActionButton,
showCompleteButton,
};
} catch (error) {
console.error("Error processing document:", error, doc);
return null; // Return null if there's an error processing this document
}
}).filter(Boolean); // Remove null entries caused by errors
return chatMessages; // Return all messages
}
return null;
} catch (error) {
console.error("Error fetching chat room details:", error);
return null;
}
}
// Call renderMessages on page load
renderMessages();
// Function to send a message to Firestore
async function sendMessage(message) {
if (!message || message.trim() === "") {
alert("Message cannot be empty");
console.log("Message cannot be empty.");
return;
}
console.log('Sending message to backend and fcm notification');
const bearerToken = "{{$userBearToken}}"; // Assuming this is properly injected on the page
const bookingId = sessionStorage.getItem('bookingId');
const apiUrl = 'https://console.fixgini.com/api/v1/chat-message';
fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearerToken, // Corrected concatenation
},
body: JSON.stringify({
message: message, // Sending message data in the body
booking_id: bookingId, // Sending bookingId data in the body
}),
})
.then(response => {
if (!response.ok) { // Check if the status is not within the range 200–299
throw new Error(`Error sending notification! status: ${response.message}`);
}
return response.json(); // Parse JSON only if the status is OK
})
.then(data => {
console.log("Notification sent successfully:", data);
// Process notification here
document.getElementById('messageInput').value = "";
const chatId = sessionStorage.getItem('chatId');
const photo = sessionStorage.getItem('photo');
const name = sessionStorage.getItem('name');
// Call the loadChatDetails function and wait for it to complete
loadChatDetails(chatId, photo, name);
window.renderMessages = renderMessages;
})
.catch(error => {
console.error("Error sending notification:", error);
});
}
// Attach event listener to the send message button
document.getElementById('sendMessageBtn').addEventListener('click', function() {
const message = document.getElementById('messageInput').value;
sendMessage(message);
});
async function handleAcceptAction(gigId, bookingId) {
const url = "https://console.fixgini.com/api/v1/booking-action";
const token = "{{$userBearToken}}"; // Replace with your actual token or dynamically retrieve it
const body = {
message: "Engagement accepted",
action: "Accepted",
gig_id: gigId,
booking_id: bookingId,
};
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
};
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(body),
});
const result = await response.json();
if (response.ok) {
alert("Booking accepted successfully!");
// Optionally refresh the UI or perform further actions
// location.reload();
} else {
alert(result.message || "An error occurred while accepting the booking.");
}
} catch (error) {
console.error("Error accepting booking:", error);
alert("An unexpected error occurred. Please try again.");
}
}
async function handleDeclineAction(gigId, bookingId) {
const url = "https://console.fixgini.com/api/v1/booking-action";
const token = "{{$userBearToken}}";
const body = {
message: "Engagement declined",
action: "declined",
gig_id: gigId,
booking_id: bookingId,
};
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
};
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(body),
});
console.log('Decline response is ', response);
const result = await response.json();
if (response.ok) {
alert("Booking declined successfully.");
// Optionally refresh the UI or perform further actions
// location.reload();
} else {
alert(result.message || "An error occurred while declining the booking.");
}
} catch (error) {
console.error("Error declining booking:", error);
alert("An unexpected error occurred. Please try again.");
}
}
// Completion button
async function handleConfirmAction(bookingId) {
const url = "https://console.fixgini.com/api/v1/complete-booking";
const token = "{{$userBearToken}}"; // Replace with your actual token or dynamically retrieve it
const body = {
message: "Engagement completion successfully",
action: "Accepted",
booking_id: bookingId,
};
console.log('body is ', body);
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
};
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(body),
});
const result = await response.json();
if (response.ok) {
alert("Engagement confirm completed successfully!");
// Optionally refresh the UI or perform further actions
// location.reload();
} else {
alert(result.message || "An error occurred while accepting the Engagement confirm completed.");
}
} catch (error) {
console.error("Error accepting Engagement confirm completed:", error);
alert("An unexpected error occurred. Please try again.");
}
}
// For Confirm Action button
document.getElementById("confirm-button").addEventListener("click", function() {
handleConfirmAction(bookingId);
});
// For Accept Action button
document.getElementById("accept-button").addEventListener("click", function() {
handleAcceptAction(gigId, bookingId);
});
// For Decline Action button
document.getElementById("decline-button").addEventListener("click", function() {
handleDeclineAction(gigId, bookingId);
});
</script>
<div class="row mb40">
{{-- Chat List --}}
<div class="col-lg-4 col-xl-4 col-xxl-4">
<div class="message_container">
<div class="d-flex justify-content-between align-items-center mb-3 mx-3 my-3">
<button class="btn btn-light-thm active rounded-pill">All</button>
<button hidden class="btn btn-light-thm rounded-pill" onclick="showStatusTab('engaged')">Engaged</button>
<button hidden class="btn btn-light-thm rounded-pill" onclick="showStatusTab('completed')">Completed</button>
</div>
<div class="inbox_user_list">
<div id="all" class="chat-member-list tab-content" style="max-height: 300px; overflow-y: auto;">
<!-- This will be populated dynamically with JavaScript -->
</div>
<div id="engaged" class="chat-member-list tab-content"
style="max-height: 300px; overflow-y: auto; display: none;">
<!-- This will be populated dynamically with JavaScript -->
</div>
<div id="completed" class="chat-member-list tab-content"
style="max-height: 300px; overflow-y: auto; display: none;">
<!-- This will be populated dynamically with JavaScript -->
</div>
</div>
</div>
</div>
<!-- Chat View -->
<div class="col-lg-8 col-xl-8 col-xxl-8">
<div class="message_container mt30-md">
<!-- Chat Header -->
<div id="user-heading" class="user_heading px-3 py-2 d-flex align-items-center">
<!-- Dynamic header content -->
</div>
<!-- Chat Messages -->
<div class="inbox_chatting_box bg-light p-3" style="height: 400px; overflow-y: auto;">
<!-- Dynamic Chat Messages -->
<div id="chat-messages-list">
<!-- Messages will dynamically populate here -->
</div>
<!-- Action Buttons (Accept/Decline) -->
<div id="action-button-container" class="mt-3">
<!-- Accept and Decline buttons will be dynamically inserted here -->
</div>
<!-- Payment Link -->
<div id="payment-link-container" class="mt-3">
<!-- Payment link will be dynamically inserted here -->
</div>
<!-- Confirm Button -->
<div id="show-complete-button-container" class="mt-3">
<!-- Confirm Completion button will be dynamically inserted here -->
</div>
</div>
<!-- Message Input Field -->
<div class="message_input_box d-flex align-items-center mt-3 mx-2 mb-2">
<input
type="text"
id="messageInput"
class="form-control flex-grow-1"
placeholder="Type a message..."
style="margin-right: 10px" />
<button
id="sendMessageBtn"
class="btn btn-primary text-white"
style="margin-right: 10px">
<i class="fas fa-paper-plane"></i>
</button>
<button hidden class="btn btn-outline-secondary text-muted">
<i class="fas fa-paperclip"></i>
</button>
</div>
</div>
</div>
</div>
</div>