Set the messages to send also the the mail and add message pannel for customers
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
2207f12276
commit
99d5af1724
@ -92,6 +92,7 @@ app.include_router(orders.router)
|
||||
app.include_router(wishlist.router)
|
||||
app.include_router(contact.router)
|
||||
app.include_router(contact.admin_router) # Admin contact messages endpoints
|
||||
app.include_router(contact.user_router) # User messages endpoints
|
||||
|
||||
# Mount static files for uploads
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
|
||||
@ -4,7 +4,9 @@ from typing import List, Optional
|
||||
from app.database.database import get_db
|
||||
from app.models import ContactMessage, User
|
||||
from app.schemas.contact import ContactMessageCreate, ContactMessageResponse, ContactMessageUpdate
|
||||
from app.services.auth import get_current_admin_user
|
||||
from app.services.auth import get_current_admin_user, get_current_user
|
||||
from app.services.email import send_contact_notification_to_admin, send_admin_response_to_customer
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter(prefix="/api/contact", tags=["contact"])
|
||||
|
||||
@ -17,6 +19,20 @@ def send_contact_message(message: ContactMessageCreate, db: Session = Depends(ge
|
||||
db.add(db_message)
|
||||
db.commit()
|
||||
db.refresh(db_message)
|
||||
|
||||
# Send email notification to admin
|
||||
try:
|
||||
send_contact_notification_to_admin(
|
||||
admin_email=settings.admin_email,
|
||||
customer_name=db_message.full_name,
|
||||
customer_email=db_message.email,
|
||||
subject=db_message.subject,
|
||||
message=db_message.message,
|
||||
phone=db_message.phone
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send admin notification email: {e}")
|
||||
|
||||
return db_message
|
||||
|
||||
|
||||
@ -81,11 +97,32 @@ def update_message(
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
update_data = message_update.model_dump(exclude_unset=True) if hasattr(message_update, 'model_dump') else message_update.dict(exclude_unset=True)
|
||||
|
||||
# Check if admin_notes is being added/updated
|
||||
send_email_to_customer = False
|
||||
if 'admin_notes' in update_data and update_data['admin_notes']:
|
||||
# Only send if there's actually content and it's different from before
|
||||
if not message.admin_notes or update_data['admin_notes'] != message.admin_notes:
|
||||
send_email_to_customer = True
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(message, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
|
||||
# Send email to customer if admin responded
|
||||
if send_email_to_customer:
|
||||
try:
|
||||
send_admin_response_to_customer(
|
||||
customer_email=message.email,
|
||||
customer_name=message.full_name,
|
||||
original_subject=message.subject,
|
||||
admin_notes=message.admin_notes
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send customer response email: {e}")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
@ -103,3 +140,38 @@ def delete_message(
|
||||
db.delete(message)
|
||||
db.commit()
|
||||
return {"message": "Contact message deleted successfully"}
|
||||
|
||||
|
||||
# User/Customer endpoints
|
||||
user_router = APIRouter(prefix="/api/my-messages", tags=["user-messages"])
|
||||
|
||||
|
||||
@user_router.get("", response_model=List[ContactMessageResponse])
|
||||
def get_my_messages(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all contact messages sent by the current user"""
|
||||
messages = db.query(ContactMessage).filter(
|
||||
ContactMessage.email == current_user.email
|
||||
).order_by(ContactMessage.created_at.desc()).all()
|
||||
return messages
|
||||
|
||||
|
||||
@user_router.get("/{message_id}", response_model=ContactMessageResponse)
|
||||
def get_my_message(
|
||||
message_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get a specific contact message by ID (only if sent by current user)"""
|
||||
message = db.query(ContactMessage).filter(
|
||||
ContactMessage.id == message_id,
|
||||
ContactMessage.email == current_user.email
|
||||
).first()
|
||||
|
||||
if not message:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
@ -231,3 +231,178 @@ Brand Master Team
|
||||
"""
|
||||
|
||||
return send_email(email, subject, body, html_body)
|
||||
|
||||
|
||||
def send_contact_notification_to_admin(admin_email: str, customer_name: str, customer_email: str, subject: str, message: str, phone: str = None) -> bool:
|
||||
"""
|
||||
Send notification to admin when a customer submits a contact message.
|
||||
|
||||
Args:
|
||||
admin_email: Admin's email address
|
||||
customer_name: Customer's full name
|
||||
customer_email: Customer's email
|
||||
subject: Message subject
|
||||
message: Message content
|
||||
phone: Customer's phone number (optional)
|
||||
|
||||
Returns:
|
||||
bool: True if email sent successfully
|
||||
"""
|
||||
email_subject = f"New Contact Message from {customer_name}"
|
||||
|
||||
# Plain text version
|
||||
body = f"""
|
||||
Hello Admin,
|
||||
|
||||
You have received a new contact message from a customer.
|
||||
|
||||
Customer Details:
|
||||
- Name: {customer_name}
|
||||
- Email: {customer_email}
|
||||
- Phone: {phone or 'Not provided'}
|
||||
|
||||
Subject: {subject}
|
||||
|
||||
Message:
|
||||
{message}
|
||||
|
||||
Please log in to the admin dashboard to view and respond to this message.
|
||||
https://brand-master.dvirlabs.com/admin
|
||||
|
||||
Best regards,
|
||||
Brand Master System
|
||||
"""
|
||||
|
||||
# HTML version
|
||||
html_body = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background: #dc3545; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }}
|
||||
.content {{ background: #f9f9f9; padding: 30px; border-radius: 0 0 5px 5px; }}
|
||||
.info-box {{ background: white; padding: 20px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #dc3545; }}
|
||||
.message-box {{ background: #fff; padding: 20px; border-radius: 5px; margin: 20px 0; border: 1px solid #ddd; }}
|
||||
.button {{ display: inline-block; padding: 12px 30px; background: #dc3545; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }}
|
||||
.footer {{ text-align: center; color: #666; margin-top: 20px; font-size: 12px; }}
|
||||
.label {{ font-weight: bold; color: #555; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📧 New Contact Message</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello Admin,</p>
|
||||
<p>You have received a new contact message from a customer.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<h3 style="margin-top: 0;">Customer Details</h3>
|
||||
<p><span class="label">Name:</span> {customer_name}</p>
|
||||
<p><span class="label">Email:</span> {customer_email}</p>
|
||||
<p><span class="label">Phone:</span> {phone or 'Not provided'}</p>
|
||||
<p><span class="label">Subject:</span> {subject}</p>
|
||||
</div>
|
||||
|
||||
<div class="message-box">
|
||||
<h3 style="margin-top: 0;">Message:</h3>
|
||||
<p>{message.replace(chr(10), '<br>')}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="https://brand-master.dvirlabs.com/admin" class="button">View in Admin Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated notification from Brand Master.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return send_email(admin_email, email_subject, body, html_body)
|
||||
|
||||
|
||||
def send_admin_response_to_customer(customer_email: str, customer_name: str, original_subject: str, admin_notes: str) -> bool:
|
||||
"""
|
||||
Send admin's response to customer's contact message.
|
||||
|
||||
Args:
|
||||
customer_email: Customer's email address
|
||||
customer_name: Customer's full name
|
||||
original_subject: Original message subject
|
||||
admin_notes: Admin's response/notes
|
||||
|
||||
Returns:
|
||||
bool: True if email sent successfully
|
||||
"""
|
||||
email_subject = f"Re: {original_subject}"
|
||||
|
||||
# Plain text version
|
||||
body = f"""
|
||||
Hello {customer_name},
|
||||
|
||||
Thank you for contacting Brand Master. We have reviewed your message and here is our response:
|
||||
|
||||
{admin_notes}
|
||||
|
||||
If you have any additional questions, please don't hesitate to contact us again or visit our website.
|
||||
|
||||
Best regards,
|
||||
Brand Master Team
|
||||
https://brand-master.dvirlabs.com
|
||||
"""
|
||||
|
||||
# HTML version
|
||||
html_body = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; padding: 30px; text-align: center; border-radius: 5px 5px 0 0; }}
|
||||
.content {{ background: #f9f9f9; padding: 30px; border-radius: 0 0 5px 5px; }}
|
||||
.response-box {{ background: white; padding: 20px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #007bff; }}
|
||||
.button {{ display: inline-block; padding: 12px 30px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }}
|
||||
.footer {{ text-align: center; color: #666; margin-top: 20px; font-size: 12px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Response to Your Message</h1>
|
||||
<p>Re: {original_subject}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello {customer_name},</p>
|
||||
<p>Thank you for contacting Brand Master. We have reviewed your message and here is our response:</p>
|
||||
|
||||
<div class="response-box">
|
||||
<h3 style="margin-top: 0;">Our Response:</h3>
|
||||
<p>{admin_notes.replace(chr(10), '<br>')}</p>
|
||||
</div>
|
||||
|
||||
<p>If you have any additional questions, please don't hesitate to contact us again.</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="https://brand-master.dvirlabs.com/contact" class="button">Contact Us Again</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Best regards, Brand Master Team</p>
|
||||
<p>This email was sent in response to your inquiry.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return send_email(customer_email, email_subject, body, html_body)
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import Orders from './pages/Orders'
|
||||
import Wishlist from './pages/Wishlist'
|
||||
import About from './pages/About'
|
||||
import Contact from './pages/Contact'
|
||||
import MyMessages from './pages/MyMessages'
|
||||
import Sales from './pages/Sales'
|
||||
import Admin from './pages/Admin'
|
||||
import Models from './pages/Models'
|
||||
@ -43,6 +44,7 @@ function App() {
|
||||
<Route path="/wishlist" element={<Wishlist />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/my-messages" element={<MyMessages />} />
|
||||
<Route path="/sales" element={<Sales />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/admin/models" element={<Models />} />
|
||||
|
||||
@ -52,6 +52,9 @@ export default function Navbar() {
|
||||
🛒
|
||||
{cart.length > 0 && <span className="cart-count">{cart.length}</span>}
|
||||
</Link>
|
||||
<Link to="/my-messages" className="icon-btn" title="My Messages">
|
||||
💬
|
||||
</Link>
|
||||
<Link to="/profile" className="icon-btn" title="Profile">
|
||||
👤
|
||||
</Link>
|
||||
|
||||
268
frontend/src/pages/MyMessages.jsx
Normal file
268
frontend/src/pages/MyMessages.jsx
Normal file
@ -0,0 +1,268 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../services/api';
|
||||
|
||||
function MyMessages() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [selectedMessage, setSelectedMessage] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
}, []);
|
||||
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.get('/my-messages');
|
||||
setMessages(response.data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Error fetching messages:', err);
|
||||
setError('Failed to load messages');
|
||||
setLoading(false);
|
||||
|
||||
if (err.response?.status === 401) {
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const statusColors = {
|
||||
new: 'bg-blue-100 text-blue-800',
|
||||
read: 'bg-yellow-100 text-yellow-800',
|
||||
replied: 'bg-green-100 text-green-800',
|
||||
};
|
||||
return statusColors[status] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-xl">Loading your messages...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">My Messages</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
View your contact messages and admin responses
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 className="mt-2 text-lg font-medium text-gray-900">
|
||||
No messages yet
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500">
|
||||
You haven't sent any contact messages yet.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/contact')}
|
||||
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Send a Message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className="bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{message.subject}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Sent on {formatDate(message.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusBadge(
|
||||
message.status
|
||||
)}`}
|
||||
>
|
||||
{message.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Original Message */}
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
||||
Your Message:
|
||||
</h4>
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-gray-700 whitespace-pre-wrap">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Response */}
|
||||
{message.admin_notes && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="text-sm font-medium text-green-700 mb-2 flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Admin Response:
|
||||
</h4>
|
||||
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
|
||||
<p className="text-gray-700 whitespace-pre-wrap">
|
||||
{message.admin_notes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!message.admin_notes && message.status === 'new' && (
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
⏳ Waiting for admin response...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message Details */}
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedMessage(
|
||||
selectedMessage?.id === message.id ? null : message
|
||||
)
|
||||
}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
||||
>
|
||||
{selectedMessage?.id === message.id
|
||||
? 'Hide Details'
|
||||
: 'Show Details'}
|
||||
<svg
|
||||
className={`w-4 h-4 ml-1 transform transition-transform ${
|
||||
selectedMessage?.id === message.id
|
||||
? 'rotate-180'
|
||||
: ''
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{selectedMessage?.id === message.id && (
|
||||
<div className="mt-4 space-y-2 text-sm">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Name:
|
||||
</span>
|
||||
<p className="text-gray-600">{message.full_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Email:
|
||||
</span>
|
||||
<p className="text-gray-600">{message.email}</p>
|
||||
</div>
|
||||
{message.phone && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Phone:
|
||||
</span>
|
||||
<p className="text-gray-600">{message.phone}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Status:
|
||||
</span>
|
||||
<p className="text-gray-600">
|
||||
{message.is_read ? 'Read' : 'Unread'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Back Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
← Back to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyMessages;
|
||||
Loading…
x
Reference in New Issue
Block a user