Set the messages to send also the the mail and add message pannel for customers
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
dvirlabs 2026-05-10 04:03:27 +03:00
parent 2207f12276
commit 99d5af1724
6 changed files with 522 additions and 1 deletions

View File

@ -92,6 +92,7 @@ app.include_router(orders.router)
app.include_router(wishlist.router) app.include_router(wishlist.router)
app.include_router(contact.router) app.include_router(contact.router)
app.include_router(contact.admin_router) # Admin contact messages endpoints app.include_router(contact.admin_router) # Admin contact messages endpoints
app.include_router(contact.user_router) # User messages endpoints
# Mount static files for uploads # Mount static files for uploads
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")

View File

@ -4,7 +4,9 @@ from typing import List, Optional
from app.database.database import get_db from app.database.database import get_db
from app.models import ContactMessage, User from app.models import ContactMessage, User
from app.schemas.contact import ContactMessageCreate, ContactMessageResponse, ContactMessageUpdate 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"]) 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.add(db_message)
db.commit() db.commit()
db.refresh(db_message) 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 return db_message
@ -81,11 +97,32 @@ def update_message(
raise HTTPException(status_code=404, detail="Message not found") 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) 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(): for field, value in update_data.items():
setattr(message, field, value) setattr(message, field, value)
db.commit() db.commit()
db.refresh(message) 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 return message
@ -103,3 +140,38 @@ def delete_message(
db.delete(message) db.delete(message)
db.commit() db.commit()
return {"message": "Contact message deleted successfully"} 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

View File

@ -231,3 +231,178 @@ Brand Master Team
""" """
return send_email(email, subject, body, html_body) 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)

View File

@ -18,6 +18,7 @@ import Orders from './pages/Orders'
import Wishlist from './pages/Wishlist' import Wishlist from './pages/Wishlist'
import About from './pages/About' import About from './pages/About'
import Contact from './pages/Contact' import Contact from './pages/Contact'
import MyMessages from './pages/MyMessages'
import Sales from './pages/Sales' import Sales from './pages/Sales'
import Admin from './pages/Admin' import Admin from './pages/Admin'
import Models from './pages/Models' import Models from './pages/Models'
@ -43,6 +44,7 @@ function App() {
<Route path="/wishlist" element={<Wishlist />} /> <Route path="/wishlist" element={<Wishlist />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} /> <Route path="/contact" element={<Contact />} />
<Route path="/my-messages" element={<MyMessages />} />
<Route path="/sales" element={<Sales />} /> <Route path="/sales" element={<Sales />} />
<Route path="/admin" element={<Admin />} /> <Route path="/admin" element={<Admin />} />
<Route path="/admin/models" element={<Models />} /> <Route path="/admin/models" element={<Models />} />

View File

@ -52,6 +52,9 @@ export default function Navbar() {
🛒 🛒
{cart.length > 0 && <span className="cart-count">{cart.length}</span>} {cart.length > 0 && <span className="cart-count">{cart.length}</span>}
</Link> </Link>
<Link to="/my-messages" className="icon-btn" title="My Messages">
💬
</Link>
<Link to="/profile" className="icon-btn" title="Profile"> <Link to="/profile" className="icon-btn" title="Profile">
👤 👤
</Link> </Link>

View 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;