fixed: webhook, edit customer

This commit is contained in:
bobbyvish
2024-09-24 18:56:42 +05:30
parent 1caad5ceb5
commit b6b4295d7e
10 changed files with 588 additions and 144 deletions

View File

@@ -401,7 +401,7 @@ class CreateCustomerForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['preferences'].queryset = EventCategory.objects.all()
class UpdateCustomerForm(forms.Form):
class UpdateOnboardedEventManagerForm(forms.Form):
first_name = forms.CharField(max_length=255, required=True, label='First Name')
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
@@ -442,5 +442,70 @@ class UpdateCustomerForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['preferences'].queryset = EventCategory.objects.all()
class UpdateEventManagerForm(forms.ModelForm):
first_name = forms.CharField(max_length=255, required=True, label='First Name')
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
phone_no = PhoneNumberField(
widget=forms.TextInput(),
label="Phone No"
)
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
city = forms.CharField(max_length=200, required=False, label="Region")
country = forms.CharField(max_length=200, required=False, label="Country")
website = forms.URLField(max_length=255, required=False, label="Website")
linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn")
facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook")
instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram")
twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter")
is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
class Meta:
model = models.IAmPrincipal
fields = [
"first_name",
"last_name",
"business_name",
"email",
"phone_no",
'address_line1',
'city',
'country',
"website",
"linkedin_profile",
"facebook_profile",
"instagram_profile",
"twitter_profile",
"is_active",
]
class UpdateEventUserForm(forms.ModelForm):
first_name = forms.CharField(max_length=255, required=True, label='First Name')
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
phone_no = PhoneNumberField(
widget=forms.TextInput(),
label="Phone No"
)
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
city = forms.CharField(max_length=200, required=False, label="Region")
country = forms.CharField(max_length=200, required=False, label="Country")
is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
class Meta:
model = models.IAmPrincipal
fields = [
"first_name",
"last_name",
"email",
"phone_no",
'address_line1',
'city',
'country',
"is_active",
]
class UploadExcelForm(forms.Form):
file = forms.FileField()

View File

@@ -21,6 +21,7 @@ from django.urls import reverse_lazy
from django.views import generic
from django.db import models, transaction, IntegrityError
from django.utils import timezone
import phonenumbers
from accounts import permission
from goodtimes import constants
from goodtimes.services import EmailService
@@ -28,6 +29,8 @@ from goodtimes.utils import JsonResponseUtil
from manage_events.models import EventCategory, PrincipalPreference
from manage_referrals.models import ReferralCode
from manage_subscriptions.models import PrincipalSubscription, Subscription
import datetime
from datetime import datetime
from . import resource_action
from .forms import (
@@ -39,7 +42,9 @@ from .forms import (
IAmPrincipalRoleAppResourceActionLinkForm,
IAmPrincipalGroupLinkForm,
ProfileEditForm,
UpdateCustomerForm,
UpdateEventManagerForm,
UpdateEventUserForm,
UpdateOnboardedEventManagerForm,
UploadExcelForm,
)
from .models import (
@@ -183,10 +188,6 @@ class PrincipalListView(LoginRequiredMixin, generic.ListView):
context["page_name"] = self.page_name
return context
import datetime
class PrincipalCreateOrUpdateView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_IAM_PRINCIPAL
model = IAmPrincipal
@@ -680,29 +681,13 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
resource = resource_action.RESOURCE_MANAGE_CUSTOMER
model = IAmPrincipal
form_class = UpdateCustomerForm
template_name = "accounts/customer/customer_edit.html"
template_name = None
success_url = reverse_lazy("accounts:customer_list")
success_message = "Updated Successfully"
error_message = "An error occurred while saving the data."
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
"operation": "Edit",
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
principal_id = kwargs.get("pk")
try:
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
except Exception as e:
messages.error(request, f"No Record of id {principal_id} is found")
return redirect(self.success_url)
print(f"principal address is {principal_obj.address_line1}")
def OnboardedEventManagerFormData(self, principal_obj):
form_class = UpdateOnboardedEventManagerForm
initial_data = {
"first_name": principal_obj.first_name,
@@ -735,7 +720,40 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
initial_data["free_start_date"] = None
initial_data["free_end_date"] = None
form = self.form_class(initial=initial_data)
return form_class(initial=initial_data)
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
"operation": "Edit",
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
principal_id = kwargs.get("pk")
try:
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
except Exception as e:
messages.error(request, f"No Record of id {principal_id} is found")
return redirect(self.success_url)
print(f"principal address is {principal_obj.address_line1}")
is_onboarded = False
if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data:
is_onboarded = principal_obj.extended_data.is_onboarded
if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER:
form = self.OnboardedEventManagerFormData(principal_obj)
self.template_name = "accounts/customer/customer_onboard_manager_edit.html"
elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER:
form = UpdateEventManagerForm(instance=principal_obj)
self.template_name = "accounts/customer/customer_manager_edit.html"
elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER:
form = UpdateEventUserForm(instance=principal_obj)
self.template_name = "accounts/customer/customer_user_edit.html"
context = self.get_context_data(form=form, principal_obj=principal_obj)
print("context dta is ", context)
return render(request, self.template_name, context=context)
@@ -747,10 +765,31 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
except Exception as e:
messages.error(request, f"No Record of customer id {principal_id} is found")
return redirect(self.success_url)
form = self.form_class(request.POST)
is_onboarded = False
if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data:
is_onboarded = principal_obj.extended_data.is_onboarded
# Dynamically choose the form based on the principal object's data
if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER:
form_class = UpdateOnboardedEventManagerForm
self.template_name = "accounts/customer/customer_onboard_manager_edit.html"
elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER:
form_class = UpdateEventManagerForm
self.template_name = "accounts/customer/customer_manager_edit.html"
elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER:
form_class = UpdateEventUserForm
self.template_name = "accounts/customer/customer_user_edit.html"
else:
messages.error(request, "Invalid principal type")
return redirect(self.success_url)
form = form_class(request.POST, instance=principal_obj)
if not form.is_valid():
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
try:
with transaction.atomic():
# update principal data
@@ -768,26 +807,27 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
principal_obj.twitter_profile = form.cleaned_data.get("twitter_profile")
principal_obj.save()
# update principal preferences record
principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj)
principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
if is_onboarded: # only update preference and subscription if it is added by admin
# update principal preferences record
principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj)
principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
# update principal subscription record
principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first()
if principal_subscription:
principal_subscription.start_date = form.cleaned_data.get("free_start_date")
principal_subscription.end_date = form.cleaned_data.get("free_end_date")
principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15)
principal_subscription.save()
else:
PrincipalSubscription.objects.create(
principal=principal_obj,
start_date=form.cleaned_data.get("free_start_date"),
end_date=form.cleaned_data.get("free_end_date"),
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
is_paid=True,
subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription
)
# update principal subscription record
principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first()
if principal_subscription:
principal_subscription.start_date = form.cleaned_data.get("free_start_date")
principal_subscription.end_date = form.cleaned_data.get("free_end_date")
principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15)
principal_subscription.save()
else:
PrincipalSubscription.objects.create(
principal=principal_obj,
start_date=form.cleaned_data.get("free_start_date"),
end_date=form.cleaned_data.get("free_end_date"),
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
is_paid=True,
subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription
)
messages.success(self.request, self.success_message)
return redirect(self.success_url)
@@ -941,10 +981,10 @@ def export_excel_template(request):
'Last Name',
'Business Name',
'Email',
'Phone No',
'Phone No (+919999999999)',
'Preferences (should be separated by comma)',
'Free period start date (YYYY-MM-DD)',
'Free period end date (YYYY-MM-DD)',
'Free period start date (DD-MM-YYYY)',
'Free period end date (DD-MM-YYYY)',
'Address',
'Region',
'Country',
@@ -976,10 +1016,10 @@ def export_excel_template(request):
['Last Name', 'The last name of the customer. This is a required field.'],
['Business Name', 'The official name of the customer\'s business or organization.'],
['Email', 'The email address of the customer. This must be a unique email not already used in the system. This is a required Field'],
['Phone No', 'The business phone number. It should include the country code if applicable.'],
['Phone No', 'The business phone number. It should include the country code if applicable (+919999999999).'],
['Category', 'A comma-separated list of event categories the customer is interested in. These should match existing categories in the system. This is a required Field'],
['Free period start date', 'The start date of the customer\'s free trial period, formatted as YYYY-MM-DD.'],
['Free period end date', 'The end date of the customer\'s free trial period, formatted as YYYY-MM-DD. This date must be later than the start date.'],
['Free period start date', 'The start date of the customer\'s free trial period, formatted as DD-MM-YYYY.'],
['Free period end date', 'The end date of the customer\'s free trial period, formatted as DD-MM-YYYY. This date must be later than the start date.'],
['Address', 'The complete business address, including street, city, and postal code.'],
['Region', 'The geographic region where the business operates.'],
['Country', 'The country where the business is based.'],
@@ -1054,6 +1094,36 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
context.update(kwargs) # Include any additional context data passed to the view
return context
def validate_date(self, date_str, row_num, error_log, field_name):
"""function to validate the date format DD-MM-YYYY"""
# Check if the input is already a datetime object
if isinstance(date_str, datetime):
return date_str
# If it's a string, attempt to validate it
if isinstance(date_str, str):
try:
return datetime.strptime(date_str, '%d-%m-%Y')
except ValueError:
error_log.append(f"Row {row_num}: {field_name} '{date_str}' is not in the format DD-MM-YYYY.")
return None
# If it's neither a string nor a datetime object, log an error
error_log.append(f"Row {row_num}: {field_name} '{date_str}' is of invalid type.")
return None
def validate_phone(self, phone_str, row_num, error_log):
"""Helper function to validate phone number"""
try:
phone_number = phonenumbers.parse(phone_str, None)
if not phonenumbers.is_valid_number(phone_number):
error_log.append(f"Row {row_num}: Phone number '{phone_str}' is not valid.")
return None
return phone_number
except Exception as e:
error_log.append(f"Row {row_num}: Phone number '{phone_str}' could not be parsed.")
return None
def get(self, request, *args, **kwargs):
form = self.form_class()
context = self.get_context_data(form=form)
@@ -1104,11 +1174,24 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
error_log.append(f"Row {idx}: Email {email} already exists.")
continue
# Validate start_date and end_date formats
start_date = self.validate_date(start_date, idx, error_log, 'Start Date')
end_date = self.validate_date(end_date, idx, error_log, 'End Date')
if not start_date or not end_date:
continue # Skip if dates are invalid
# validate date rnage
if end_date < start_date:
error_log.append(f"Row {idx}: End date {end_date} must greater then start date {start_date}.")
continue
# Validate phone number
if phone_no:
phone_number = self.validate_phone(str(phone_no), idx, error_log)
if not phone_number:
continue # Skip if phone number is invalid
# validate preferences
preference_list = [pref.strip() for pref in preferences.split(',')]
event_categories = EventCategory.objects.filter(title__in=preference_list)

View File

@@ -589,49 +589,51 @@ class GoogleMapsservice:
Returns:
QuerySet: The filtered and sorted queryset of events.
"""
# Set the origin to the provided latitude and longitude
origins = [(latitude, longitude)]
# Create a list of destination coordinates for all events with valid venues
destinations = [
(event.venue.latitude, event.venue.longitude)
# Create a list of destination coordinates and map them to the events
destinations_and_events = [
((event.venue.latitude, event.venue.longitude), event)
for event in queryset
if event.venue.latitude and event.venue.longitude
]
# If there is no destination coordinates
if not destinations:
if not destinations_and_events:
return queryset
# Get the distance matrix from the Google Maps API
matrix = self.get_distance_matrix(origins, destinations)
# Batch size for Google Distance Matrix API (max 25 elements)
batch_size = 25
distances = {}
# Create a dictionary of event IDs and their corresponding distances
distances = {
event.id: element["distance"]["value"]
for event, element in zip(queryset, matrix["rows"][0]["elements"])
if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000 # Convert km to meters
}
# Loop through batches of destinations
for i in range(0, len(destinations_and_events), batch_size):
batch = destinations_and_events[i:i + batch_size]
print(f"batch list count is {len(batch)}")
destinations = [coords for coords, _ in batch]
events_in_batch = [event for _, event in batch]
print(f"distance is {distances} and distances key is {distances.keys()}")
# Call the Google Maps API for the current batch
matrix = self.get_distance_matrix(origins, destinations)
# Extract distances and associate them with events
for event, element in zip(events_in_batch, matrix["rows"][0]["elements"]):
if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000: # Convert km to meters
distances[event.id] = element["distance"]["value"]
if not distances:
return queryset.none()
# Filter the queryset to include only events within the specified radius
queryset = queryset.filter(id__in=distances.keys())
print(f"query set after distance filter {queryset}")
# Sort the event IDs by their distances in ascending order
event_ids_by_distance = sorted(distances, key=distances.get)
print(f"sort event by it distance {event_ids_by_distance}")
# Create a Case/When expression to preserve the order of events by distance
preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)])
print(f"preserved_order is {preserved_order}")
# Order the queryset based on the preserved order
queryset = queryset.order_by(preserved_order)
for data in queryset:
print(f"queryset after preserverd order {data.id}")
return queryset

View File

@@ -22,7 +22,7 @@ class WebhookService:
def _fetch_metadata(self):
"""Fetch metadata based on the event type."""
if self._event_type == "checkout.session.completed":
if self._event_type in ["checkout.session.expired", "checkout.session.completed"]:
return self._charge_data.get("metadata", {})
elif self._event_type == "invoice.payment_succeeded":
subscription_id = self._charge_data.get("subscription")

View File

@@ -121,48 +121,6 @@ class CreatePrincipalSubscriptionApi(APIView):
return ApiResponse.error(**fail_response)
# class CreatePrincipalSubscriptionApi(APIView):
# authentication_classes = [JWTAuthentication]
# permission_classes = [IsAuthenticated]
# def post(self, request):
# serializer = PrincipalSubscriptionSerializer(data=request.data)
# if serializer.is_valid():
# subscription_id = serializer.validated_data.get("subscription").id
# try:
# subscription = Subscription.objects.get(id=subscription_id)
# except Subscription.DoesNotExist:
# return ApiResponse.error(
# status=status.HTTP_404_NOT_FOUND, message="Subscription not found."
# )
# start_date = timezone.localtime().date()
# end_date = start_date + timedelta(days=subscription.plan.days)
# grace_period_end_date = end_date + timedelta(days=15)
# # You can directly pass the additional fields as save method arguments
# instance = serializer.save(
# start_date=start_date,
# end_date=end_date,
# grace_period_end_date=grace_period_end_date,
# created_by=request.user, # Assuming your model has this field and you want to track who created the subscription
# )
# success_response = {
# "status": status.HTTP_201_CREATED, # Use 201 for successful resource creation
# "message": "Success",
# "data": serializer.data,
# }
# return ApiResponse.success(**success_response)
# else:
# fail_response = {
# "status": status.HTTP_400_BAD_REQUEST,
# "message": "Validation Failed",
# "errors": serializer.errors,
# }
# return ApiResponse.error(**fail_response)
@method_decorator(csrf_exempt, name="dispatch")
@@ -177,28 +135,29 @@ class StripeWebhookTest(APIView):
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
endpoint_secret = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f" # Make sure to retrieve this from your settings
event = None
webhook_event = None
try:
# Construct Stripe event
event = stripe.Event.construct_from(json.loads(payload), stripe.api_key)
event_id = event["id"]
event_type = event["type"]
stripe_subscription_id = event["data"]["object"].get("subscription")
# Retrieve subscription details if available
stripe_subscription = (
stripe.Subscription.retrieve(stripe_subscription_id)
if stripe_subscription_id
else None
)
current_period_start = (
stripe_subscription["current_period_start"]
if stripe_subscription
else None
)
current_period_end = (
stripe_subscription["current_period_end"]
if stripe_subscription
else None
)
current_period_start = stripe_subscription["current_period_start"] if stripe_subscription else None
current_period_end = stripe_subscription["current_period_end"] if stripe_subscription else None
# Log received event details
logger.info(f"Received event {event_type} with ID {event_id}")
# Get or create WebhookEvent in DB
webhook_event, created = WebhookEvent.objects.get_or_create(
event_id=event_id,
defaults={
@@ -208,59 +167,80 @@ class StripeWebhookTest(APIView):
)
if not created and webhook_event.status == "processed":
logger.info(f"Event {event_id} already processed.")
return ApiResponse.success(
status=status.HTTP_208_ALREADY_REPORTED,
message="Event already processed",
message="Event already processed.",
)
# Process the event
payment_service = PaymentProcessingService(
webhook_data=event,
stripe_subscription=stripe_subscription_id,
current_period_start=current_period_start,
current_period_end=current_period_end,
)
payment_service.process_event()
# Mark event as successfully processed
webhook_event.status = "processed"
webhook_event.processed_at = timezone.now()
webhook_event.save()
logger.info(f"Event {event_id} processed successfully.")
return ApiResponse.success(
status=status.HTTP_200_OK, message="Event processed successfully"
status=status.HTTP_200_OK, message="Event processed successfully."
)
except stripe.error.SignatureVerificationError as e:
logger.error(f"Invalid Stripe signature: {str(e)}")
logger.error(f"Invalid Stripe signature for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid signature",
message="Invalid signature.",
errors=str(e),
)
except ValueError as e:
logger.error(f"Invalid payload: {str(e)}")
logger.error(f"Invalid payload for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid payload",
message="Invalid payload.",
errors=str(e),
)
except Transaction.DoesNotExist as e:
logger.error(f"Transaction does not exist: {str(e)}")
except stripe.error.InvalidRequestError as e:
logger.error(f"Invalid request for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_404_NOT_FOUND,
message="Transaction not found",
status=status.HTTP_400_BAD_REQUEST,
message="Invalid request to Stripe.",
errors=str(e),
)
except stripe.error.StripeError as e:
logger.error(f"General Stripe error: {str(e)}")
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Stripe error occurred.",
errors=str(e),
)
except Exception as e:
logger.error(f"Error processing webhook event: {str(e)}")
logger.error(f"Unexpected error processing event {event_id}: {str(e)}")
if "webhook_event" in locals():
webhook_event.status = "failed"
webhook_event.error_message = str(e)
webhook_event.save()
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Error processing event",
message="Unexpected error processing event.",
errors=str(e),
)
finally:
print(f"finally is runn")
webhook_event.status = "processed"
webhook_event.processed_at = timezone.now()
webhook_event.save()
class LastActiveSubscriptionView(APIView):

View File

@@ -535,6 +535,7 @@ def create_checkout_session(request):
"metadata": {
"transaction_amount": str(subscription.amount),
"principal": str(principal_id),
"principal_email": str(request.user.email),
"subscription_id": str(subscription.id),
"product_id": subscription.product_id,
"couponCode": coupon_code if coupon_code else None,

View File

@@ -129,7 +129,6 @@
</td> -->
<td class="text-center">
<ul class="table-controls">
{% if data_obj.extended_data.is_onboarded%}
<li><a href="{% url 'accounts:customer_edit' data_obj.id%}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
@@ -142,7 +141,6 @@
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
{%endif%}
<li><a href="{% url 'accounts:customer_detail' data_obj.id%}" class="bs-tooltip" data-bs-toggle="tooltip" data-bs-placement="top" title="" data-original-title="View" data-bs-original-title="View" aria-label="View">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a></li>

View File

@@ -0,0 +1,162 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% include "cdn_through_html/flatpicker_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Customer</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" id="addCustomer">
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/flatpicker_cdn_js.html" %}
{% include "cdn_through_html/jquery_validate_cdn_js.html" %}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
-->
<script>
$(document).ready(function() {
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
// minDate: "today",
onChange: function(selectedDates, dateStr, instance) {
end_date.set('minDate', selectedDates[0]);
}
});
var end_date = flatpickr(document.getElementById('id_free_end_date'), {
minDate: null // initialize with no minimum date
});
$('.js-example-basic-multiple').select2({
placeholder: 'Select options',
allowClear: true,
tokenSeparators: [',', ' '], // Customize token separators
closeOnSelect: false // Keep the dropdown open after selection
});
// Add custom validation method for email
$.validator.addMethod("validEmail", function(value, element) {
return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(value);
}, "Please enter a valid email address.");
// Add custom validation method to check for special characters
$.validator.addMethod("noSpecialChars", function(value, element) {
return /^[a-zA-Z\s]*$/.test(value); // Allow only letters and whitespace
}, "Please enter only letters and spaces.");
// Add custom validation method to check for starting with a letter
$.validator.addMethod("startsWithLetter", function(value, element) {
return /^[a-zA-Z]/.test(value); // Check if the value starts with a letter
}, "Please start with a letter.");
// Add custom validation method for greater than start date
$.validator.addMethod("greaterThanStartDate", function(value, element) {
var startDate = $('#id_free_start_date').val();
if (!startDate || !value) {
return true;
}
return new Date(value) > new Date(startDate);
}, "The end date must be after the start date.");
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
maxlength:15,
noSpecialChars: true,
startsWithLetter: true,
},
last_name: {
required: true,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
},
email: {
required: true,
validEmail: true,
},
},
messages: {
first_name: {
required: "Please enter your first name.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "First name must start with a letter."
},
last_name: {
required: "Please enter your last name.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "Last name must start with a letter."
},
email: {
required: "Please enter your email address.",
validEmail: "Please enter a valid email address."
},
},
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
$(element).closest('.form-group').append(error);
},
highlight: function(element, errorClass, validClass) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element, errorClass, validClass) {
$(element).removeClass('is-invalid').addClass('is-valid');
},
submitHandler: function(form) {
// Check if form is valid before submission
if ($(form).valid()) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
}
});
})
</script>
{% endblock %}

View File

@@ -0,0 +1,153 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% include "cdn_through_html/flatpicker_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Customer</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" id="addCustomer">
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/flatpicker_cdn_js.html" %}
{% include "cdn_through_html/jquery_validate_cdn_js.html" %}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
-->
<script>
$(document).ready(function() {
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
// minDate: "today",
onChange: function(selectedDates, dateStr, instance) {
end_date.set('minDate', selectedDates[0]);
}
});
var end_date = flatpickr(document.getElementById('id_free_end_date'), {
minDate: null // initialize with no minimum date
});
$('.js-example-basic-multiple').select2({
placeholder: 'Select options',
allowClear: true,
tokenSeparators: [',', ' '], // Customize token separators
closeOnSelect: false // Keep the dropdown open after selection
});
// Add custom validation method for email
$.validator.addMethod("validEmail", function(value, element) {
return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(value);
}, "Please enter a valid email address.");
// Add custom validation method to check for special characters
$.validator.addMethod("noSpecialChars", function(value, element) {
return /^[a-zA-Z\s]*$/.test(value); // Allow only letters and whitespace
}, "Please enter only letters and spaces.");
// Add custom validation method to check for starting with a letter
$.validator.addMethod("startsWithLetter", function(value, element) {
return /^[a-zA-Z]/.test(value); // Check if the value starts with a letter
}, "Please start with a letter.");
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
maxlength:15,
noSpecialChars: true,
startsWithLetter: true,
},
last_name: {
required: true,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
},
email: {
required: true,
validEmail: true,
},
},
messages: {
first_name: {
required: "Please enter your first name.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "First name must start with a letter."
},
last_name: {
required: "Please enter your last name.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "Last name must start with a letter."
},
email: {
required: "Please enter your email address.",
validEmail: "Please enter a valid email address."
},
},
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
$(element).closest('.form-group').append(error);
},
highlight: function(element, errorClass, validClass) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element, errorClass, validClass) {
$(element).removeClass('is-invalid').addClass('is-valid');
},
submitHandler: function(form) {
// Check if form is valid before submission
if ($(form).valid()) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
}
});
})
</script>
{% endblock %}