Compare commits
No commits in common. "28e232b5dc956a9c25ca0007ab5b0db83f006ded" and "63f8be59e85c18b6feb1512f881d291e094005e0" have entirely different histories.
28e232b5dc
...
63f8be59e8
@ -350,33 +350,6 @@ def api_root(request, format=None):
|
|||||||
"has_jitsi_meeting": "true"
|
"has_jitsi_meeting": "true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reschedule_appointment": {
|
|
||||||
"description": "Reschedule an existing appointment (Admin only)",
|
|
||||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/reschedule/"),
|
|
||||||
"methods": ["POST"],
|
|
||||||
"authentication": "Required (Staff users only)",
|
|
||||||
"required_fields": ["scheduled_datetime"],
|
|
||||||
"optional_fields": ["scheduled_duration", "date_str", "time_slot"],
|
|
||||||
"prerequisites": "Appointment must be in 'scheduled' status",
|
|
||||||
"scheduling_options": {
|
|
||||||
"direct_datetime": {
|
|
||||||
"example": {
|
|
||||||
"scheduled_datetime": "2025-12-10T11:00:00Z",
|
|
||||||
"scheduled_duration": 45,
|
|
||||||
"timezone": timezone.get_current_timezone_name()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"date_and_slot": {
|
|
||||||
"example": {"date_str": "2024-01-20", "time_slot": "afternoon", "scheduled_duration": 60}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"validation": "Validates against admin availability when using date_str + time_slot",
|
|
||||||
"side_effects": [
|
|
||||||
"Updates scheduled_datetime and scheduled_duration",
|
|
||||||
"Clears Jitsi meeting information",
|
|
||||||
"Sends rescheduled email to user"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"reject_appointment": {
|
"reject_appointment": {
|
||||||
"description": "Reject an appointment request (Admin only)",
|
"description": "Reject an appointment request (Admin only)",
|
||||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/reject/"),
|
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/reject/"),
|
||||||
@ -417,18 +390,6 @@ def api_root(request, format=None):
|
|||||||
"Sends completion email to user"
|
"Sends completion email to user"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"cancel_meeting": {
|
|
||||||
"description": "Cancel a scheduled appointment and its Jitsi meeting",
|
|
||||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/cancel/"),
|
|
||||||
"methods": ["POST"],
|
|
||||||
"authentication": "Required",
|
|
||||||
"prerequisites": "Appointment must be in 'scheduled' or 'active' status",
|
|
||||||
"side_effects": [
|
|
||||||
"Updates meeting status to 'cancelled'",
|
|
||||||
"Clears Jitsi meeting information",
|
|
||||||
"Sends cancellation email to user",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"appointment_stats": {
|
"appointment_stats": {
|
||||||
"description": "Get appointment statistics and analytics with availability metrics (Admin only)",
|
"description": "Get appointment statistics and analytics with availability metrics (Admin only)",
|
||||||
|
|||||||
@ -91,42 +91,6 @@ class EmailService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to send scheduled notification: {e}")
|
print(f"Failed to send scheduled notification: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def send_appointment_rescheduled(appointment):
|
|
||||||
subject = "Your Appointment Has Been Rescheduled"
|
|
||||||
|
|
||||||
user_timezone = getattr(appointment, 'user_timezone', 'UTC')
|
|
||||||
|
|
||||||
formatted_datetime = EmailService._format_datetime_for_user(
|
|
||||||
appointment.scheduled_datetime,
|
|
||||||
user_timezone
|
|
||||||
)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'appointment': appointment,
|
|
||||||
'scheduled_datetime': formatted_datetime,
|
|
||||||
'user_dashboard_url': f"{settings.FRONTEND_URL}/dashboard" if hasattr(settings, 'FRONTEND_URL') else '/dashboard/',
|
|
||||||
'settings': {
|
|
||||||
'SITE_NAME': getattr(settings, 'SITE_NAME', 'Attune Heart Therapy')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html_message = render_to_string('emails/appointment_rescheduled.html', context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
email = EmailMultiAlternatives(
|
|
||||||
subject=subject,
|
|
||||||
body=f"Your appointment has been rescheduled for {formatted_datetime}. Please view this email in an HTML-compatible client for more details.",
|
|
||||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
||||||
to=[appointment.email],
|
|
||||||
)
|
|
||||||
email.attach_alternative(html_message, "text/html")
|
|
||||||
email.send(fail_silently=False)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to send rescheduled notification: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_appointment_rejected(appointment):
|
def send_appointment_rejected(appointment):
|
||||||
|
|||||||
@ -662,6 +662,7 @@ class AppointmentRequest(models.Model):
|
|||||||
|
|
||||||
def cancel_appointment(self, reason='', commit=True):
|
def cancel_appointment(self, reason='', commit=True):
|
||||||
self.status = 'cancelled'
|
self.status = 'cancelled'
|
||||||
|
self.rejection_reason = reason
|
||||||
if commit:
|
if commit:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@ -242,6 +242,7 @@ class AppointmentRequestCreateSerializer(serializers.ModelSerializer):
|
|||||||
ZoneInfo(value)
|
ZoneInfo(value)
|
||||||
return value
|
return value
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# If invalid, default to UTC but don't raise error
|
||||||
return 'UTC'
|
return 'UTC'
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -322,10 +323,13 @@ class AppointmentRequestCreateSerializer(serializers.ModelSerializer):
|
|||||||
return time_slots
|
return time_slots
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
# Extract timezone before creating
|
||||||
timezone = validated_data.pop('timezone', 'UTC')
|
timezone = validated_data.pop('timezone', 'UTC')
|
||||||
|
|
||||||
|
# Create appointment
|
||||||
appointment = super().create(validated_data)
|
appointment = super().create(validated_data)
|
||||||
|
|
||||||
|
# Set timezone on the created appointment
|
||||||
appointment.user_timezone = timezone
|
appointment.user_timezone = timezone
|
||||||
appointment.save(update_fields=['user_timezone'])
|
appointment.save(update_fields=['user_timezone'])
|
||||||
|
|
||||||
@ -434,35 +438,6 @@ class AppointmentScheduleSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
class RecheduleAppointmentSerializer(serializers.Serializer):
|
|
||||||
new_scheduled_datetime = serializers.DateTimeField()
|
|
||||||
new_scheduled_duration = serializers.IntegerField(default=60, min_value=30, max_value=240)
|
|
||||||
timezone = serializers.CharField(required=False, default='UTC')
|
|
||||||
|
|
||||||
def validate_new_scheduled_datetime(self, value):
|
|
||||||
if value <= timezone.now():
|
|
||||||
raise serializers.ValidationError("New scheduled datetime must be in the future.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_new_scheduled_duration(self, value):
|
|
||||||
if value < 30:
|
|
||||||
raise serializers.ValidationError("Duration must be at least 30 minutes.")
|
|
||||||
if value > 240:
|
|
||||||
raise serializers.ValidationError("Duration cannot exceed 4 hours.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def save(self, appointment):
|
|
||||||
new_datetime = self.validated_data['new_scheduled_datetime']
|
|
||||||
new_duration = self.validated_data.get('new_scheduled_duration', 60)
|
|
||||||
|
|
||||||
appointment.reschedule_appointment(
|
|
||||||
new_datetime=new_datetime,
|
|
||||||
new_duration=new_duration,
|
|
||||||
commit=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return appointment
|
|
||||||
|
|
||||||
class AppointmentDetailSerializer(serializers.ModelSerializer):
|
class AppointmentDetailSerializer(serializers.ModelSerializer):
|
||||||
meeting_info = serializers.SerializerMethodField()
|
meeting_info = serializers.SerializerMethodField()
|
||||||
meeting_analytics = serializers.SerializerMethodField()
|
meeting_analytics = serializers.SerializerMethodField()
|
||||||
|
|||||||
@ -18,9 +18,7 @@ from .views import (
|
|||||||
MeetingAnalyticsView,
|
MeetingAnalyticsView,
|
||||||
availability_overview,
|
availability_overview,
|
||||||
EndMeetingView,
|
EndMeetingView,
|
||||||
StartMeetingView,
|
StartMeetingView
|
||||||
CancelMeetingView,
|
|
||||||
RescheduleAppointmentView
|
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -35,7 +33,6 @@ urlpatterns = [
|
|||||||
# Appointment Request URLs
|
# Appointment Request URLs
|
||||||
path('appointments/', AppointmentRequestListView.as_view(), name='appointment-list'),
|
path('appointments/', AppointmentRequestListView.as_view(), name='appointment-list'),
|
||||||
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
||||||
path('appointments/<uuid:pk>/reschedule/', RescheduleAppointmentView.as_view(), name='reschedule-appointment'),
|
|
||||||
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
||||||
path('appointments/<uuid:pk>/matching-availability/', MatchingAvailabilityView.as_view(), name='matching-availability'),
|
path('appointments/<uuid:pk>/matching-availability/', MatchingAvailabilityView.as_view(), name='matching-availability'),
|
||||||
|
|
||||||
@ -60,5 +57,4 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('meetings/<uuid:pk>/end/', EndMeetingView.as_view(), name='end-meeting-simple'),
|
path('meetings/<uuid:pk>/end/', EndMeetingView.as_view(), name='end-meeting-simple'),
|
||||||
path('meetings/<uuid:pk>/start/', StartMeetingView.as_view(), name='start-meeting-simple'),
|
path('meetings/<uuid:pk>/start/', StartMeetingView.as_view(), name='start-meeting-simple'),
|
||||||
path('meetings/<uuid:pk>/cancel/', CancelMeetingView.as_view(), name='cancel-meeting'),
|
|
||||||
]
|
]
|
||||||
@ -18,7 +18,6 @@ from .serializers import (
|
|||||||
AppointmentDetailSerializer,
|
AppointmentDetailSerializer,
|
||||||
MeetingJoinSerializer,
|
MeetingJoinSerializer,
|
||||||
MeetingActionSerializer,
|
MeetingActionSerializer,
|
||||||
RecheduleAppointmentSerializer
|
|
||||||
)
|
)
|
||||||
from .email_service import EmailService
|
from .email_service import EmailService
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
@ -181,36 +180,6 @@ class RejectAppointmentView(generics.GenericAPIView):
|
|||||||
response_serializer = AppointmentDetailSerializer(appointment)
|
response_serializer = AppointmentDetailSerializer(appointment)
|
||||||
return Response(response_serializer.data)
|
return Response(response_serializer.data)
|
||||||
|
|
||||||
class RescheduleAppointmentView(generics.GenericAPIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
||||||
serializer_class = RecheduleAppointmentSerializer
|
|
||||||
queryset = AppointmentRequest.objects.all()
|
|
||||||
lookup_field = 'pk'
|
|
||||||
|
|
||||||
def post(self, request, pk):
|
|
||||||
appointment = self.get_object()
|
|
||||||
|
|
||||||
if appointment.status != 'scheduled':
|
|
||||||
return Response(
|
|
||||||
{'error': 'Only scheduled appointments can be rescheduled'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
new_datetime = serializer.validated_data['new_scheduled_datetime']
|
|
||||||
new_duration = serializer.validated_data.get('new_scheduled_duration', appointment.scheduled_duration)
|
|
||||||
|
|
||||||
appointment.reschedule_appointment(
|
|
||||||
new_datetime=new_datetime,
|
|
||||||
new_duration=new_duration
|
|
||||||
)
|
|
||||||
|
|
||||||
EmailService.send_appointment_rescheduled(appointment)
|
|
||||||
|
|
||||||
response_serializer = AppointmentDetailSerializer(appointment)
|
|
||||||
return Response(response_serializer.data)
|
|
||||||
|
|
||||||
class AvailableDatesView(generics.GenericAPIView):
|
class AvailableDatesView(generics.GenericAPIView):
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
@ -509,34 +478,6 @@ class EndMeetingView(generics.GenericAPIView):
|
|||||||
'meeting_ended_at': appointment.meeting_ended_at,
|
'meeting_ended_at': appointment.meeting_ended_at,
|
||||||
})
|
})
|
||||||
|
|
||||||
class CancelMeetingView(generics.GenericAPIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
serializer_class = MeetingActionSerializer
|
|
||||||
queryset = AppointmentRequest.objects.all()
|
|
||||||
lookup_field = 'pk'
|
|
||||||
|
|
||||||
def post(self, request, pk):
|
|
||||||
appointment = self.get_object()
|
|
||||||
|
|
||||||
if not request.user.is_staff:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Only staff members can cancel meetings'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
if appointment.status != 'scheduled':
|
|
||||||
return Response(
|
|
||||||
{'error': 'Only scheduled appointments can be canceled'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
appointment.cancel_appointment()
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'appointment_id': str(appointment.id),
|
|
||||||
'message': 'Meeting canceled successfully',
|
|
||||||
})
|
|
||||||
|
|
||||||
class JoinMeetingView(generics.GenericAPIView):
|
class JoinMeetingView(generics.GenericAPIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
serializer_class = MeetingJoinSerializer
|
serializer_class = MeetingJoinSerializer
|
||||||
|
|||||||
@ -1,294 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Appointment Confirmed</title>
|
|
||||||
<style>
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
Roboto, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-header {
|
|
||||||
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
|
|
||||||
padding: 40px 30px;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-header h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-header p {
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.9;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-body {
|
|
||||||
padding: 40px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greeting {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-card {
|
|
||||||
background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 25px 0;
|
|
||||||
border: 2px dashed #38a169;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-title {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #22543d;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appointment-time {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #22543d;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.therapist-info {
|
|
||||||
color: #2d3748;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card {
|
|
||||||
background: #f7fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
color: #2d3748;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preparation {
|
|
||||||
background: #fffaf0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 25px;
|
|
||||||
margin: 25px 0;
|
|
||||||
border-left: 4px solid #ed8936;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prep-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #744210;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prep-list {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prep-list li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-left: 25px;
|
|
||||||
position: relative;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prep-list li:before {
|
|
||||||
content: "✓";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
color: #48bb78;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info {
|
|
||||||
background: #edf2f7;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-section {
|
|
||||||
text-align: center;
|
|
||||||
margin: 35px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
|
|
||||||
color: white;
|
|
||||||
padding: 16px 32px;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-info {
|
|
||||||
color: #2d3748;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer {
|
|
||||||
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-text {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 15px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.email-container {
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-header,
|
|
||||||
.email-body,
|
|
||||||
.email-footer {
|
|
||||||
padding: 25px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-card {
|
|
||||||
padding: 25px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appointment-time {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-icon {
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="email-container">
|
|
||||||
<div class="email-header">
|
|
||||||
<h1>Appointment Time Updated!</h1>
|
|
||||||
<p>Your therapy session has been rescheduled</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="email-body">
|
|
||||||
<div class="greeting">
|
|
||||||
Hello <strong>{{ appointment.full_name }}</strong>,<br />
|
|
||||||
Great news! Your appointment request has been confirmed. We're looking
|
|
||||||
forward to seeing you.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="confirmation-card">
|
|
||||||
<div class="confirmation-title">Your Appointment Details</div>
|
|
||||||
<div class="appointment-time">
|
|
||||||
{{ scheduled_datetime }}
|
|
||||||
</div>
|
|
||||||
<div class="therapist-info">With: Nathalie (Therapist)</div>
|
|
||||||
<div class="login-info">
|
|
||||||
On the day of your appointment, please log in to your account at https://attuneHeartTherapy.com 15 minutes early to join your Therapy Session.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="email-footer">
|
|
||||||
<div class="company-name">{{ settings.SITE_NAME }}</div>
|
|
||||||
<p class="support-info">
|
|
||||||
Need help? Contact our support team at
|
|
||||||
|
|
||||||
<a href="mailto:admin@attunehearttherapy.com"
|
|
||||||
style="color: #fff; text-decoration: none"
|
|
||||||
>admin@attunehearttherapy.com</a>
|
|
||||||
</p>
|
|
||||||
<p class="copyright">
|
|
||||||
© {% now "Y" %} {{ settings.SITE_NAME }}. All rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue
Block a user