mobile app apis

This commit is contained in:
Flatlogic Bot 2026-01-26 17:46:01 +00:00
parent 59561573fc
commit e27928f933
76 changed files with 2703 additions and 54 deletions

View File

@ -49,12 +49,15 @@ CSRF_COOKIE_SAMESITE = "None"
# Application definition
INSTALLED_APPS = [
'jazzmin',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'core',
]
@ -223,4 +226,83 @@ if HOST_FQDN:
else:
SITE_URL = HOST_FQDN
else:
SITE_URL = "http://127.0.0.1:8000"
SITE_URL = "http://127.0.0.1:8000"
# Jazzmin Settings
JAZZMIN_SETTINGS = {
"site_title": "Masar Express Admin",
"site_header": "Masar Express",
"site_brand": "Masar Express",
"site_logo": "img/logo.jpg",
"login_logo": "img/logo.jpg",
"welcome_sign": "Welcome to Masar Express Admin",
"copyright": "Masar Express",
"search_model": ["core.Parcel", "auth.User"],
"user_avatar": None,
"topmenu_links": [
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
{"model": "auth.User"},
{"app": "core"},
],
"usermenu_links": [
{"model": "auth.User"}
],
"show_sidebar": True,
"navigation_expanded": True,
"hide_apps": [],
"hide_models": [],
"order_with_respect_to": ["core", "auth"],
"icons": {
"auth": "fas fa-users-cog",
"auth.user": "fas fa-user",
"auth.Group": "fas fa-users",
"core.Parcel": "fas fa-box",
"core.Profile": "fas fa-id-card",
"core.PlatformProfile": "fas fa-building",
"core.Country": "fas fa-globe",
"core.City": "fas fa-city",
"core.DriverRating": "fas fa-star",
"core.Testimonial": "fas fa-comment",
},
"default_icon_parents": "fas fa-chevron-circle-right",
"default_icon_children": "fas fa-circle",
"related_modal_active": False,
"custom_css": "css/custom.css",
"custom_js": None,
"use_google_fonts_cdn": True,
"show_ui_builder": False,
}
JAZZMIN_UI_TWEAKS = {
"navbar_small_text": False,
"footer_small_text": False,
"body_small_text": False,
"brand_small_text": False,
"brand_colour": False,
"accent": "accent-primary",
"navbar": "navbar-white navbar-light",
"no_navbar_border": False,
"navbar_fixed": False,
"layout_boxed": False,
"footer_fixed": False,
"sidebar_fixed": True,
"sidebar": "sidebar-dark-primary",
"sidebar_nav_small_text": False,
"theme": "flatly",
"dark_mode_theme": None,
"button_classes": {
"primary": "btn-primary",
"secondary": "btn-secondary",
"info": "btn-info",
"warning": "btn-warning",
"danger": "btn-danger",
"success": "btn-success"
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
}

Binary file not shown.

Binary file not shown.

80
core/api_views.py Normal file
View File

@ -0,0 +1,80 @@
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.views import APIView
from django.db.models import Q
from .models import Parcel, Profile
from .serializers import ParcelSerializer, ProfileSerializer
class CustomAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
# Ensure profile exists
profile, created = Profile.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user_id': user.pk,
'email': user.email,
'role': profile.role,
'username': user.username
})
class ParcelListCreateView(generics.ListCreateAPIView):
serializer_class = ParcelSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
profile = user.profile
if profile.role == 'shipper':
return Parcel.objects.filter(shipper=user).order_by('-created_at')
elif profile.role == 'car_owner':
# Drivers see available parcels (pending) or their own assignments
return Parcel.objects.filter(
Q(status='pending') | Q(carrier=user)
).order_by('-created_at')
else:
return Parcel.objects.none()
def perform_create(self, serializer):
# Only shippers can create
if self.request.user.profile.role != 'shipper':
raise permissions.PermissionDenied("Only shippers can create parcels.")
serializer.save(shipper=self.request.user)
class ParcelDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ParcelSerializer
permission_classes = [permissions.IsAuthenticated]
queryset = Parcel.objects.all()
def get_queryset(self):
# Restrict access
user = self.request.user
if user.profile.role == 'shipper':
return Parcel.objects.filter(shipper=user)
elif user.profile.role == 'car_owner':
# Drivers can see parcels they can accept (pending) or are assigned to
return Parcel.objects.filter(
Q(status='pending') | Q(carrier=user)
)
return Parcel.objects.none()
def perform_update(self, serializer):
# Add logic: Drivers can only update status, Shippers can edit details if pending
# For simplicity in this v1, we allow updates but validation should be improved for production
serializer.save()
class UserProfileView(generics.RetrieveUpdateAPIView):
serializer_class = ProfileSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
return self.request.user.profile

37
core/serializers.py Normal file
View File

@ -0,0 +1,37 @@
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Parcel, Profile, Governate, City
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
class ProfileSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = Profile
fields = ['id', 'user', 'role', 'phone_number', 'address', 'profile_picture']
class GovernateSerializer(serializers.ModelSerializer):
class Meta:
model = Governate
fields = ['id', 'name_en', 'name_ar']
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = ['id', 'name_en', 'name_ar']
class ParcelSerializer(serializers.ModelSerializer):
pickup_governate_detail = GovernateSerializer(source='pickup_governate', read_only=True)
pickup_city_detail = CitySerializer(source='pickup_city', read_only=True)
delivery_governate_detail = GovernateSerializer(source='delivery_governate', read_only=True)
delivery_city_detail = CitySerializer(source='delivery_city', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Parcel
fields = '__all__'
read_only_fields = ['shipper', 'tracking_number', 'created_at', 'updated_at', 'thawani_session_id']

View File

@ -20,6 +20,9 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-history-tab" data-bs-toggle="pill" data-bs-target="#pills-history" type="button" role="tab">{% trans "Transaction History" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-cancelled-tab" data-bs-toggle="pill" data-bs-target="#pills-cancelled" type="button" role="tab">{% trans "Cancelled Shipments" %}</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
@ -191,7 +194,7 @@
{% endif %}
</div>
<!-- Transaction History -->
<!-- Transaction History (Delivered Only) -->
<div class="tab-pane fade" id="pills-history" role="tabpanel">
{% if completed_parcels %}
<div class="card shadow-sm border-0" style="border-radius: 15px;">
@ -217,7 +220,7 @@
<td>{{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</td>
<td>{{ parcel.price }} OMR</td>
<td>
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% else %}bg-danger{% endif %}">
<span class="badge bg-success">
{{ parcel.get_status_display }}
</span>
</td>
@ -234,6 +237,50 @@
</div>
{% endif %}
</div>
<!-- Cancelled Shipments -->
<div class="tab-pane fade" id="pills-cancelled" role="tabpanel">
{% if cancelled_parcels %}
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">{% trans "Date" %}</th>
<th>{% trans "Tracking ID" %}</th>
<th>{% trans "From" %}</th>
<th>{% trans "To" %}</th>
<th>{% trans "Price" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for parcel in cancelled_parcels %}
<tr>
<td class="ps-4">{{ parcel.created_at|date:"Y-m-d" }}</td>
<td><span class="badge bg-light text-dark">#{{ parcel.tracking_number }}</span></td>
<td>{{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</td>
<td>{{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</td>
<td>{{ parcel.price }} OMR</td>
<td>
<span class="badge bg-danger">
{{ parcel.get_status_display }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="text-center py-5">
<p class="lead">{% trans "No cancelled shipments." %}</p>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -16,6 +16,9 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-history-tab" data-bs-toggle="pill" data-bs-target="#pills-history" type="button" role="tab">{% trans "Transaction History" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-cancelled-tab" data-bs-toggle="pill" data-bs-target="#pills-cancelled" type="button" role="tab">{% trans "Cancelled Shipments" %}</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
@ -221,9 +224,9 @@
{% endif %}
</div>
<!-- Transaction History (Table View) -->
<!-- Transaction History (Delivered Only) -->
<div class="tab-pane fade" id="pills-history" role="tabpanel">
{% if history_parcels %}
{% if delivered_parcels %}
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body p-0">
<div class="table-responsive">
@ -240,7 +243,7 @@
</tr>
</thead>
<tbody>
{% for parcel in history_parcels %}
{% for parcel in delivered_parcels %}
{% get_rating parcel as rating %}
<tr>
<td class="ps-4">{{ parcel.created_at|date:"Y-m-d" }}</td>
@ -255,7 +258,7 @@
</td>
<td>{{ parcel.price }} OMR</td>
<td>
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% else %}bg-danger{% endif %}">
<span class="badge bg-success">
{{ parcel.get_status_display }}
</span>
</td>
@ -293,6 +296,70 @@
</div>
{% endif %}
</div>
<!-- Cancelled Shipments -->
<div class="tab-pane fade" id="pills-cancelled" role="tabpanel">
{% if cancelled_parcels %}
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">{% trans "Date" %}</th>
<th>{% trans "Tracking ID" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Carrier" %}</th>
<th>{% trans "Bid/Price" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for parcel in cancelled_parcels %}
<tr>
<td class="ps-4">{{ parcel.created_at|date:"Y-m-d" }}</td>
<td><span class="badge bg-light text-dark">#{{ parcel.tracking_number }}</span></td>
<td>{{ parcel.description|truncatechars:30 }}</td>
<td>
{% if parcel.carrier %}
{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}
{% else %}
-
{% endif %}
</td>
<td>{{ parcel.price }} OMR</td>
<td>
<span class="badge bg-danger">
{{ parcel.get_status_display }}
</span>
</td>
<td>
<!-- Actions available for cancelled parcels -->
<!-- Maybe view label or invoice if paid? -->
<a href="{% url 'generate_parcel_label' parcel.id %}" class="btn btn-sm btn-outline-dark me-2" target="_blank" title="{% trans 'Print Label' %}">
<i class="fas fa-print"></i>
</a>
{% if parcel.payment_status == 'paid' %}
<a href="{% url 'generate_invoice' parcel.id %}" class="btn btn-sm btn-outline-secondary me-2" target="_blank" title="{% trans 'Invoice' %}">
<i class="fas fa-file-invoice"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="text-center py-5">
<p class="lead">{% trans "No cancelled shipments." %}</p>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
from . import api_views
urlpatterns = [
path('', views.index, name='index'),
@ -64,4 +65,10 @@ urlpatterns = [
# OTP Login
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
]
# API Endpoints
path('api/auth/token/', api_views.CustomAuthToken.as_view(), name='api_token_auth'),
path('api/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'),
path('api/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'),
path('api/profile/', api_views.UserProfileView.as_view(), name='api_user_profile'),
]

View File

@ -160,7 +160,10 @@ def dashboard(request):
if profile.role == 'shipper':
all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at')
active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled'])
history_parcels = all_parcels.filter(status__in=['delivered', 'cancelled'])
# Split history into delivered and cancelled
delivered_parcels = all_parcels.filter(status='delivered')
cancelled_parcels = all_parcels.filter(status='cancelled')
# Pagination for Active Shipments
page = request.GET.get('page', 1)
@ -178,7 +181,8 @@ def dashboard(request):
return render(request, 'core/shipper_dashboard.html', {
'active_parcels': active_parcels,
'history_parcels': history_parcels,
'delivered_parcels': delivered_parcels,
'cancelled_parcels': cancelled_parcels,
'payments_enabled': payments_enabled,
'platform_profile': platform_profile # Pass full profile just in case
})
@ -206,13 +210,17 @@ def dashboard(request):
# Active: Picked up or In Transit
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at')
# History: Delivered or Cancelled
completed_parcels = Parcel.objects.filter(carrier=request.user, status__in=['delivered', 'cancelled']).order_by('-created_at')
# History: Delivered
completed_parcels = Parcel.objects.filter(carrier=request.user, status='delivered').order_by('-created_at')
# Cancelled
cancelled_parcels = Parcel.objects.filter(carrier=request.user, status='cancelled').order_by('-created_at')
return render(request, 'core/driver_dashboard.html', {
'available_parcels': available_parcels,
'my_parcels': my_parcels,
'completed_parcels': completed_parcels
'completed_parcels': completed_parcels,
'cancelled_parcels': cancelled_parcels
})
@login_required
@ -880,4 +888,4 @@ def cancel_parcel(request, parcel_id):
parcel.save()
messages.success(request, _("Shipment cancelled successfully."))
return redirect('dashboard')
return redirect('dashboard')

Binary file not shown.

View File

@ -1390,3 +1390,6 @@ msgstr "مسح رمز الاستجابة السريعة"
msgid "Tracking No"
msgstr "رقم التتبع"
msgid "Cancelled Shipments"
msgstr "الشحنات الملغاة"

View File

@ -3,4 +3,6 @@ mysqlclient==2.2.7
python-dotenv==1.1.1
Pillow
weasyprint
qrcode
qrcode
django-jazzmin==3.0.1
djangorestframework==3.15.1

BIN
static/img/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,29 +1,17 @@
'use strict';
{
// Call function fn when the DOM is loaded and ready. If it is already
// loaded, call the function now.
// http://youmightnotneedjquery.com/#ready
function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
(function($) {
'use strict';
ready(function() {
function handleClick(event) {
event.preventDefault();
const params = new URLSearchParams(window.location.search);
if (params.has('_popup')) {
window.close(); // Close the popup.
$(document).ready(function() {
$('.cancel-link').click(function(e) {
e.preventDefault();
const parentWindow = window.parent;
if (parentWindow && typeof(parentWindow.dismissRelatedObjectModal) === 'function' && parentWindow !== window) {
parentWindow.dismissRelatedObjectModal();
} else {
window.history.back(); // Otherwise, go back.
// fallback to default behavior
window.history.back();
}
}
document.querySelectorAll('.cancel-link').forEach(function(el) {
el.addEventListener('click', handleClick);
return false;
});
});
}
})(django.jQuery);

View File

@ -1,15 +1,44 @@
'use strict';
{
const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
switch(initData.action) {
case 'change':
opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value);
break;
case 'delete':
opener.dismissDeleteRelatedObjectPopup(window, initData.value);
break;
default:
opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
break;
(function() {
'use strict';
var windowRef = window;
var windowRefProxy;
var windowName, widgetName;
var openerRef = windowRef.opener;
if (!openerRef) {
// related modal is active
openerRef = windowRef.parent;
windowName = windowRef.name;
widgetName = windowName.replace(/^(change|add|delete|lookup)_/, '');
windowRefProxy = {
name: widgetName,
location: windowRef.location,
close: function() {
openerRef.dismissRelatedObjectModal();
}
};
windowRef = windowRefProxy;
}
}
// default django popup_response.js
var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
switch (initData.action) {
case 'change':
if (typeof(openerRef.dismissChangeRelatedObjectPopup) === 'function') {
openerRef.dismissChangeRelatedObjectPopup(windowRef, initData.value, initData.obj, initData.new_value);
}
break;
case 'delete':
if (typeof(openerRef.dismissDeleteRelatedObjectPopup) === 'function') {
openerRef.dismissDeleteRelatedObjectPopup(windowRef, initData.value);
}
break;
default:
if (typeof(openerRef.dismissAddRelatedObjectPopup) === 'function') {
openerRef.dismissAddRelatedObjectPopup(windowRef, initData.value, initData.obj);
}
break;
}
})();

View File

@ -103,4 +103,62 @@ h1, h2, h3, h4, h5, h6 {
.status-pending { background: #FFE8CC; color: #D9480F; }
.status-picked_up { background: #E3FAFC; color: #0B7285; }
.status-in_transit { background: #E7F5FF; color: #1864AB; }
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
/* Chat Widget */
#masar-chat-widget {
position: fixed;
bottom: 90px;
right: 20px;
width: 350px;
height: 500px;
z-index: 9999;
display: flex;
flex-direction: column;
}
#masar-chat-toggle {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
background-color: var(--accent-orange);
color: white;
border: none;
transition: transform 0.2s;
}
#masar-chat-toggle:hover {
transform: scale(1.05);
}
/* RTL Support */
[dir="rtl"] #masar-chat-widget {
right: auto;
left: 20px;
}
[dir="rtl"] #masar-chat-toggle {
right: auto;
left: 20px;
}
.typing-dots span {
display: inline-block;
width: 8px;
height: 8px;
background-color: #adb5bd;
border-radius: 50%;
margin: 0 2px;
animation: typing 1s infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}

BIN
staticfiles/img/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,927 @@
/** Django-related improvements to AdminLTE UI **/
div.inline-related {
padding: 10px;
}
.form-row {
padding: 5px;
}
.help-block ul {
margin: 10px 0 0 15px;
padding: 0;
}
/** Fix bug of adminLTE, since django is using th headers in middle of table **/
.card-body.p-0 .table thead > tr > th:first-of-type,
.card-body.p-0 .table thead > tr > td:first-of-type,
.card-body.p-0 .table tfoot > tr > th:first-of-type,
.card-body.p-0 .table tfoot > tr > td:first-of-type,
.card-body.p-0 .table tbody > tr > th:first-of-type,
.card-body.p-0 .table tbody > tr > td:first-of-type {
padding-left: 0.75rem;
}
.card-body.p-0 .table thead > tr > th:last-of-type,
.card-body.p-0 .table thead > tr > td:last-of-type,
.card-body.p-0 .table tfoot > tr > th:last-of-type,
.card-body.p-0 .table tfoot > tr > td:last-of-type,
.card-body.p-0 .table tbody > tr > th:last-of-type,
.card-body.p-0 .table tbody > tr > td:last-of-type {
padding-right: 0.75rem;
}
.card-body.p-0 .table thead > tr > th:first-child,
.card-body.p-0 .table thead > tr > td:first-child,
.card-body.p-0 .table tfoot > tr > th:first-child,
.card-body.p-0 .table tfoot > tr > td:first-child,
.card-body.p-0 .table tbody > tr > th:first-child,
.card-body.p-0 .table tbody > tr > td:first-child {
padding-left: 1.5rem;
}
.card-body.p-0 .table thead > tr > th:last-child,
.card-body.p-0 .table thead > tr > td:last-child,
.card-body.p-0 .table tfoot > tr > th:last-child,
.card-body.p-0 .table tfoot > tr > td:last-child,
.card-body.p-0 .table tbody > tr > th:last-child,
.card-body.p-0 .table tbody > tr > td:last-child {
padding-right: 1.5rem;
}
[class*=sidebar-dark-] .nav-header {
color: rgb(255,255,255,0.3);
margin-top: 1rem;
}
/* Table styles */
.table tr.form-row {
display: table-row;
}
.table td.action-checkbox {
width: 45px;
}
.table thead th {
color: #64748b;
border-bottom: 0;
}
.empty-form {
display: none !important;
}
.inline-related .tabular {
background-color: white;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
td.djn-td,
th.djn-th {
padding: 10px;
}
td.delete input {
margin: 10px;
}
tr.djn-tr>.original {
padding-left: 20px;
}
.hidden {
display: none;
}
/* Checkbox selection table header */
.djn-checkbox-select-all {
padding-right: 0 !important;
width: 0;
}
.object-tools {
padding: 0;
}
.object-tools li {
list-style: none;
margin: 0;
padding: 0;
}
.object-tools .historylink {
background-color: #3c8dbc;
width: 100%;
display: block;
padding: 5px;
text-align: center;
color: white;
}
.jazzmin-avatar {
font-size: 20px;
}
.related-widget-wrapper-link {
padding: 7px;
}
.related-widget-wrapper select {
width: initial;
/* Setting a width will make the *-related btns overflow */
height: auto;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
box-shadow: inset 0 0 0 transparent;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.tab-pane {
overflow-x: auto;
}
table.dataTable thead .sorting::after,
table.dataTable thead .sorting_asc::after,
table.dataTable thead .sorting_desc::after,
table.dataTable thead .sorting_asc_disabled::after,
table.dataTable thead .sorting_desc_disabled::after {
right: 0.5em;
content: "\2193";
}
.select2-container {
min-width: 200px;
}
.select2-container .select2-selection--single {
border: 1px solid #ced4da !important;
min-height: 38px;
/* Center text inside */
display: flex !important;
align-items: center;
}
.select2-container--default .select2-selection--single {
border: 1px solid #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
right: 5px !important;
top: unset !important;
}
.select2-results__option {
color: black;
}
#changelist-search .form-group {
margin-bottom: .5em;
margin-right: .5em;
}
.table tbody tr th {
padding-left: .75rem;
}
.user-profile {
font-size: 2.4em;
}
.date-hierarchy {
margin-right: 8px;
display: block;
}
/* APP.CSS */
.form-group div .vTextField,
.form-group div .vLargeTextField,
.form-group div .vURLField,
.form-group div .vBigIntegerField,
.form-group div input[type="text"]
{
display: block;
width: 100%;
}
.vTextField,
.vLargeTextField,
.vURLField,
.vIntegerField,
.vBigIntegerField,
.vForeignKeyRawIdAdminField,
.vDateField,
.vTimeField,
input[type="number"],
input[type="text"]
{
height: calc(2.25rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
box-shadow: inset 0 0 0 transparent;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.vDateField,
.vTimeField {
margin-bottom: 5px;
display: inline-block;
}
.vLargeTextField {
height: auto;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
}
.date-icon:before,
.clock-icon:before {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome !important;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
content: "\f073";
}
.clock-icon:before {
content: "\f017";
}
/* CALENDARS & CLOCKS */
.calendarbox,
.clockbox {
margin: 5px auto;
font-size: 12px;
width: 19em;
text-align: center;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
.clockbox {
width: auto;
}
.calendar {
margin: 0;
padding: 0;
}
.calendar table {
margin: 0;
padding: 0;
border-collapse: collapse;
background: white;
width: 100%;
}
.calendar caption,
.calendarbox h2,
.clockbox h2 {
margin: 0;
text-align: center;
border-top: none;
background: #f5dd5d;
font-weight: 700;
font-size: 12px;
color: #333;
}
.clockbox h2 {
font-size: 16px;
padding: 5px;
}
.calendar th {
padding: 8px 5px;
background: #f8f8f8;
border-bottom: 1px solid #ddd;
font-weight: 400;
font-size: 12px;
text-align: center;
color: #666;
}
.calendar td {
font-weight: 400;
font-size: 12px;
text-align: center;
padding: 0;
border-top: 1px solid #eee;
border-bottom: none;
}
.calendar td.selected a {
background: #3C8DBC;
color: #fff !important;
}
.calendar td.nonday {
background: #f8f8f8;
}
.calendar td.today a {
font-weight: 700;
}
.calendar td a,
.timelist a {
display: block;
font-weight: 400;
padding: 6px;
text-decoration: none;
color: #444;
}
.calendar td a:focus,
.timelist a:focus,
.calendar td a:hover,
.timelist a:hover {
background: #3C8DBC;
color: white;
}
.calendar td a:active,
.timelist a:active {
background: #3C8DBC;
color: white;
}
.calendarnav {
font-size: 10px;
text-align: center;
color: #ccc;
margin: 0;
padding: 1px 3px;
}
.calendarnav a:link,
#calendarnav a:visited,
#calendarnav a:focus,
#calendarnav a:hover {
color: #999;
}
.calendar-shortcuts {
background: white;
font-size: 11px;
line-height: 11px;
border-top: 1px solid #eee;
padding: 8px 0;
color: #ccc;
}
.calendarbox .calendarnav-previous,
.calendarbox .calendarnav-next {
display: block;
position: absolute;
top: 8px;
width: 15px;
height: 15px;
text-indent: -9999px;
padding: 0;
}
.calendarnav-previous {
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarbox .calendarnav-previous:focus,
.calendarbox .calendarnav-previous:hover {
background-position: 0 -15px;
}
.calendarnav-next {
right: 10px;
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
}
.calendarbox .calendarnav-next:focus,
.calendarbox .calendarnav-next:hover {
background-position: 0 -45px;
}
.calendar-cancel {
margin: 0;
padding: 4px 0;
font-size: 12px;
background: #eee;
border-top: 1px solid #ddd;
color: #333;
}
.calendar-cancel:focus,
.calendar-cancel:hover {
background: #ddd;
}
.calendar-cancel a {
color: black;
display: block;
}
/* Selectors - This needs some work TODO */
.selector {
width: 100%;
float: left;
}
.selector select {
width: 100%;
height: 15em;
}
.selector-available,
.selector-chosen {
float: left;
width: 48%;
text-align: center;
margin-bottom: 5px;
}
.selector-available h2,
.selector-chosen h2 {
border: 1px solid #ccc;
font-size: 16px;
padding: 5px;
}
.selector-chosen h2 {
background: #007bff;
color: #fff;
}
.selector .selector-available h2 {
background: #f8f8f8;
color: #666;
}
.selector .selector-filter {
background: white;
border: 1px solid #ccc;
padding: 8px;
color: #999;
font-size: 10px;
margin: 0;
text-align: left;
}
.selector-filter input {
height: 24px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
margin-left: 0 !important;
}
.selector .selector-filter label,
.inline-group .aligned .selector .selector-filter label {
float: left;
margin: 0;
width: 18px;
height: 18px;
padding: 0;
overflow: hidden;
line-height: 1;
}
/* Might need to import more rules from:
* https://github.com/django/django/blob/master/django/contrib/admin/static/admin/css/responsive.css
*/
.inline-group {
overflow: auto;
}
.selector .selector-available input {
width: 100%;
margin-left: 8px;
}
.selector ul.selector-chooser {
float: left;
width: 4%;
background-color: #eee;
border-radius: 10px;
margin: 10em 0 0;
padding: 0;
}
.selector-chooser li {
margin: 0;
padding: 3px;
list-style-type: none;
}
.selector select {
padding: 0 10px;
margin: 0 0 10px;
/*border-radius: 0 0 4px 4px;*/
;
}
.selector-add,
.selector-remove {
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.3;
}
.active.selector-add,
.active.selector-remove {
opacity: 1;
}
.active.selector-add:hover,
.active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus,
.active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-remove:focus,
.active.selector-remove:hover {
background-position: 0 -80px;
}
a.selector-chooseall,
a.selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
margin: 1px auto 3px;
overflow: hidden;
font-weight: bold;
line-height: 16px;
color: #666;
text-decoration: none;
opacity: 0.3;
}
a.active.selector-chooseall:focus,
a.active.selector-clearall:focus,
a.active.selector-chooseall:hover,
a.active.selector-clearall:hover {
color: #447e9b;
}
a.active.selector-chooseall,
a.active.selector-clearall {
opacity: 1;
}
a.active.selector-chooseall:hover,
a.active.selector-clearall:hover {
cursor: pointer;
}
a.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}
a.active.selector-chooseall:focus,
a.active.selector-chooseall:hover {
background-position: 100% -176px;
}
a.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}
a.active.selector-clearall:focus,
a.active.selector-clearall:hover {
background-position: 0 -144px;
}
.selector .search-label-icon {
height: 0;
}
#user_form input[type="password"] {
width: 100%;
}
.control-label {
margin-top: 7px;
}
.help-block,
.timezonewarning {
font-size: .8em;
color: #859099;
font-style: italic;
}
.dashboard tbody tr:first-child td {
border-top: none;
}
.vTimeField {
margin-top: 10px;
}
.vTimeField,
.vDateField {
min-width: 200px;
}
.date-icon::before,
.clock-icon::before {
font-family: "Font Awesome 5 Free" !important;
}
.timelist li {
list-style-type: none;
}
.timelist {
margin: 0;
padding: 0;
}
body.no-sidebar .content-wrapper,
body.no-sidebar .main-footer,
body.no-sidebar .main-header {
margin-left: 0;
}
.vCheckboxLabel.inline {
vertical-align: top;
color: red;
margin-bottom: 0;
}
.inline-related .card-header>span {
float: right;
}
.ui-customiser .menu-items div {
width: 40px;
height: 20px;
border-radius: 25px;
margin-right: 10px;
margin-bottom: 10px;
opacity: 0.8;
cursor: pointer;
}
.ui-customiser select {
width: 100%;
height: auto;
padding: 6px 2px;
}
.control-sidebar-content label {
vertical-align: top;
}
.ui-customiser .menu-items div.inactive {
opacity: 0.3;
}
.ui-customiser .menu-items div.active {
opacity: 1;
border: 1px solid white;
}
.timeline-item {
word-break: break-word;
}
.navbar-nav .brand-link {
padding-top: 3px;
}
.breadcrumb {
background: transparent;
margin: 0;
}
.breadcrumb-item+.breadcrumb-item::before {
content: "\203A";
}
.login-box,
.register-box {
width: 500px;
max-width: 100%;
}
#jazzy-collapsible .collapsible-header:hover {
background: #007bff;
color: white;
}
#jazzy-collapsible .collapsible-header {
cursor: pointer;
}
#jazzy-carousel .carousel-indicators li {
background-color: #007bfe;
}
#jazzy-carousel .carousel-indicators {
position: initial;
}
form ul.radiolist li {
list-style-type: none;
}
form ul.radiolist label {
float: none;
display: inline;
}
form ul.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
.content-wrapper>.content {
padding: 1rem 2rem;
}
.navbar {
padding: .5rem 2rem;
}
.main-footer {
color: #869099;
padding: 1rem 2rem;
font-size: 14px;
}
.page-actions > a {
margin-right:0.25rem;
margin-left: 0.25rem;
}
#jazzy-actions.sticky-top {
top: 10px;
}
body.layout-navbar-fixed #jazzy-actions.sticky-top {
top: 67px;
}
/* stacked inlines */
a.inline-deletelink:hover {
background-color: #c82333;
border-color: #bd2130;
}
a.inline-deletelink {
float: right;
padding: 3px 5px;
margin: 10px;
background-color: #dc3545;
border-radius: .25rem;
color: white !important;
border: 1px solid #dc3545;
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
/* end stacked inlines */
/* Support for django-mptt */
#result_list .field-tree_actions {
width: calc(40px + 2.25rem);
}
#result_list .field-tree_actions>div {
margin-top: 0;
}
/* End support for django-mptt */
/* modal tweaks */
.modal.modal-wide .modal-dialog {
width: 50%;
max-width: inherit;
}
.modal-wide .modal-body {
overflow-y: auto;
}
iframe.related-iframe {
width: 100%;
height: 450px;
}
/* Blur background when using modal */
.modal-open .wrapper {
-webkit-filter: blur(1px);
-moz-filter: blur(1px);
-o-filter: blur(1px);
-ms-filter: blur(1px);
filter: blur(1px);
}
/* end modal tweaks */
.control-sidebar {
overflow: hidden scroll;
}
/* tweaks to allow bootstrap styling */
body.jazzmin-login-page {
-ms-flex-align: center;
align-items: center;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
height: 100vh;
-ms-flex-pack: center;
justify-content: center;
}
.callout {
color: black;
}
/* sidebar scrolling */
.layout-fixed #jazzy-sidebar {
top: 0;
bottom: 0;
/* Enable y scroll */
overflow-y: scroll;
/* May inherit scroll, so we need to explicitly hide */
overflow-x: hidden;
}
/* calculate height to fit content, we don't to enable scrolling if the content fits */
.layout-fixed #jazzy-sidebar .sidebar {
height: auto !important;
}
/* Hide scrollbar */
.layout-fixed #jazzy-sidebar {
scrollbar-width: none;
}
.layout-fixed #jazzy-sidebar::-webkit-scrollbar {
width: 0;
}
/* nav-item will overflow container in width if scrollbar is visible */
#jazzy-sidebar .nav-sidebar > .nav-item {
width: 100%;
}
/* tweeks for django-filer*/
.navigator-top-nav + #content-main {
float: left;
width: 100%;
}

View File

@ -0,0 +1,14 @@
<svg width="15" height="60" viewBox="0 0 1792 7168" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="previous">
<path d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="next">
<path d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#previous" x="0" y="0" fill="#333333" />
<use xlink:href="#previous" x="0" y="1792" fill="#000000" />
<use xlink:href="#next" x="0" y="3584" fill="#333333" />
<use xlink:href="#next" x="0" y="5376" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,9 @@
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="icon">
<path d="M192 1664h288v-288h-288v288zm352 0h320v-288h-320v288zm-352-352h288v-320h-288v320zm352 0h320v-320h-320v320zm-352-384h288v-288h-288v288zm736 736h320v-288h-320v288zm-384-736h320v-288h-320v288zm768 736h288v-288h-288v288zm-384-352h320v-320h-320v320zm-352-864v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm736 864h288v-320h-288v320zm-384-384h320v-288h-320v288zm384 0h288v-288h-288v288zm32-480v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm384-64v1280q0 52-38 90t-90 38h-1408q-52 0-90-38t-38-90v-1280q0-52 38-90t90-38h128v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h384v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h128q52 0 90 38t38 90z"/>
</g>
</defs>
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#efb80b" d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -0,0 +1,34 @@
<svg width="16" height="192" viewBox="0 0 1792 21504" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="up">
<path d="M1412 895q0-27-18-45l-362-362-91-91q-18-18-45-18t-45 18l-91 91-362 362q-18 18-18 45t18 45l91 91q18 18 45 18t45-18l189-189v502q0 26 19 45t45 19h128q26 0 45-19t19-45v-502l189 189q19 19 45 19t45-19l91-91q18-18 18-45zm252 1q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="down">
<path d="M1412 897q0-27-18-45l-91-91q-18-18-45-18t-45 18l-189 189v-502q0-26-19-45t-45-19h-128q-26 0-45 19t-19 45v502l-189-189q-19-19-45-19t-45 19l-91 91q-18 18-18 45t18 45l362 362 91 91q18 18 45 18t45-18l91-91 362-362q18-18 18-45zm252-1q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="left">
<path d="M1408 960v-128q0-26-19-45t-45-19h-502l189-189q19-19 19-45t-19-45l-91-91q-18-18-45-18t-45 18l-362 362-91 91q-18 18-18 45t18 45l91 91 362 362q18 18 45 18t45-18l91-91q18-18 18-45t-18-45l-189-189h502q26 0 45-19t19-45zm256-64q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="right">
<path d="M1413 896q0-27-18-45l-91-91-362-362q-18-18-45-18t-45 18l-91 91q-18 18-18 45t18 45l189 189h-502q-26 0-45 19t-19 45v128q0 26 19 45t45 19h502l-189 189q-19 19-19 45t19 45l91 91q18 18 45 18t45-18l362-362 91-91q18-18 18-45zm251 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="clearall">
<path transform="translate(336, 336) scale(0.75)" d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="chooseall">
<path transform="translate(336, 336) scale(0.75)" d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#up" x="0" y="0" fill="#666666" />
<use xlink:href="#up" x="0" y="1792" fill="#447e9b" />
<use xlink:href="#down" x="0" y="3584" fill="#666666" />
<use xlink:href="#down" x="0" y="5376" fill="#447e9b" />
<use xlink:href="#left" x="0" y="7168" fill="#666666" />
<use xlink:href="#left" x="0" y="8960" fill="#447e9b" />
<use xlink:href="#right" x="0" y="10752" fill="#666666" />
<use xlink:href="#right" x="0" y="12544" fill="#447e9b" />
<use xlink:href="#clearall" x="0" y="14336" fill="#666666" />
<use xlink:href="#clearall" x="0" y="16128" fill="#447e9b" />
<use xlink:href="#chooseall" x="0" y="17920" fill="#666666" />
<use xlink:href="#chooseall" x="0" y="19712" fill="#447e9b" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,151 @@
(function($) {
'use strict';
function FixSelectorHeight() {
$('.selector .selector-chosen').each(function () {
let selector_chosen = $(this);
let selector_available = selector_chosen.siblings('.selector-available');
let selector_chosen_select = selector_chosen.find('select').first();
let selector_available_select = selector_available.find('select').first();
let selector_available_filter = selector_available.find('p.selector-filter').first();
selector_chosen_select.height(selector_available_select.height() + selector_available_filter.outerHeight());
selector_chosen_select.css('border-top', selector_chosen_select.css('border-bottom'));
});
}
function handleCarousel($carousel) {
const errors = $('.errorlist li', $carousel);
const hash = document.location.hash;
// If we have errors, open that tab first
if (errors.length) {
const errorCarousel = errors.eq(0).closest('.carousel-item');
$carousel.carousel(errorCarousel.data('carouselid'));
$('.carousel-fieldset-label', $carousel).text(errorCarousel.data()["label"]);
} else if (hash) {
// If we have a tab hash, open that
const activeCarousel = $('.carousel-item[data-target="' + hash + '"]', $carousel);
$carousel.carousel(activeCarousel.data()["carouselid"]);
$('.carousel-fieldset-label', $carousel).text(activeCarousel.data()["label"]);
}
// Update page hash/history on slide
$carousel.on('slide.bs.carousel', function (e) {
FixSelectorHeight();
// call resize in change view after tab switch
window.dispatchEvent(new Event('resize'));
if (e.relatedTarget.dataset.hasOwnProperty("label")) {
$('.carousel-fieldset-label', $carousel).text(e.relatedTarget.dataset.label);
}
const hash = e.relatedTarget.dataset.target;
if (history.pushState) {
history.pushState(null, null, hash);
} else {
location.hash = hash;
}
});
}
function handleTabs($tabs) {
const errors = $('.change-form .errorlist li');
const hash = document.location.hash;
// If we have errors, open that tab first
if (errors.length) {
const tabId = errors.eq(0).closest('.tab-pane').attr('id');
$('a[href="#' + tabId + '"]').tab('show');
} else if (hash) {
// If we have a tab hash, open that
$('a[href="' + hash + '"]', $tabs).tab('show');
}
// Change hash for page-reload
$('a', $tabs).on('shown.bs.tab', function (e) {
FixSelectorHeight();
// call resize in change view after tab switch
window.dispatchEvent(new Event('resize'));
e.preventDefault();
if (history.pushState) {
history.pushState(null, null, e.target.hash);
} else {
location.hash = e.target.hash;
}
});
}
function handleCollapsible($collapsible) {
const errors = $('.errorlist li', $collapsible);
const hash = document.location.hash;
// If we have errors, open that tab first
if (errors.length) {
$('.panel-collapse', $collapsible).collapse('hide');
errors.eq(0).closest('.panel-collapse').collapse('show');
} else if (hash) {
// If we have a tab hash, open that
$('.panel-collapse', $collapsible).collapse('hide');
$(hash, $collapsible).collapse('show');
}
// Change hash for page-reload
$collapsible.on('shown.bs.collapse', function (e) {
FixSelectorHeight();
// call resize in change view after tab switch
window.dispatchEvent(new Event('resize'));
if (history.pushState) {
history.pushState(null, null, '#' + e.target.id);
} else {
location.hash = '#' + e.target.id;
}
});
}
function applySelect2() {
// Apply select2 to any select boxes that don't yet have it
// and are not part of the django's empty-form inline
const noSelect2 = '.empty-form select, .select2-hidden-accessible, .selectfilter, .selector-available select, .selector-chosen select, select[data-autocomplete-light-function=select2]';
$('select').not(noSelect2).select2({ width: 'element' });
}
$(document).ready(function () {
const $carousel = $('#content-main form #jazzy-carousel');
const $tabs = $('#content-main form #jazzy-tabs');
const $collapsible = $('#content-main form #jazzy-collapsible');
// Ensure all raw_id_fields have the search icon in them
$('.related-lookup').append('<i class="fa fa-search"></i>');
// Style the inline fieldset button
$('.inline-related fieldset.module .add-row a').addClass('btn btn-sm btn-default float-right');
$('div.add-row>a').addClass('btn btn-sm btn-default float-right');
// Ensure we preserve the tab the user was on using the url hash, even on page reload
if ($tabs.length) { handleTabs($tabs); }
else if ($carousel.length) { handleCarousel($carousel); }
else if ($collapsible.length) { handleCollapsible($collapsible); }
applySelect2();
$('body').on('change', '.related-widget-wrapper select', function(e) {
const event = $.Event('django:update-related');
$(this).trigger(event);
if (!event.isDefaultPrevented() && typeof(window.updateRelatedObjectLinks) !== 'undefined') {
updateRelatedObjectLinks(this);
}
});
});
// Apply select2 to all select boxes when new inline row is created
django.jQuery(document).on('formset:added', applySelect2);
})(jQuery);

View File

@ -0,0 +1,64 @@
(function($) {
'use strict';
$.fn.search_filters = function () {
$(this).change(function () {
const $field = $(this);
const $option = $field.find('option:selected');
const select_name = $option.data('name');
if (select_name) {
$field.attr('name', select_name);
} else {
$field.removeAttr('name');
}
});
$(this).trigger('change');
};
function getMinimuInputLength(element) {
return window.filterInputLength[element.data('name')] ?? window.filterInputLengthDefault;
}
function searchFilters() {
// Make search filters select2 and ensure they work for filtering
const $ele = $('.search-filter');
$ele.search_filters();
$ele.each(function () {
const $this = $(this);
$this.select2({ width: '100%', minimumInputLength: getMinimuInputLength($this) });
});
// Use select2 for mptt dropdowns
const $mptt = $('.search-filter-mptt');
if ($mptt.length) {
$mptt.search_filters();
$mptt.select2({
width: '100%',
minimumInputLength: getMinimuInputLength($mptt),
templateResult: function (data) {
// https://stackoverflow.com/questions/30820215/selectable-optgroups-in-select2#30948247
// rewrite templateresult for build tree hierarchy
if (!data.element) {
return data.text;
}
const $element = $(data.element);
let $wrapper = $('<span></span>');
$wrapper.attr('style', $($element[0]).attr('style'));
$wrapper.text(data.text);
return $wrapper;
},
});
}
}
$(document).ready(function () {
// Ensure all raw_id_fields have the search icon in them
$('.related-lookup').append('<i class="fa fa-search"></i>')
// Allow for styling of selects
$('.actions select').addClass('form-control').select2({ width: 'element' });
searchFilters();
});
})(jQuery);

View File

@ -0,0 +1,67 @@
(function($) {
'use strict';
function setCookie(key, value) {
const expires = new Date();
expires.setTime(expires.getTime() + (value * 24 * 60 * 60 * 1000));
document.cookie = key + '=' + value + ';expires=' + expires.toUTCString() + '; SameSite=Strict;path=/';
}
function getCookie(key) {
const keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
function handleMenu() {
$('[data-widget=pushmenu]').bind('click', function () {
const menuClosed = getCookie('jazzy_menu') === 'closed';
if (!menuClosed) {
setCookie('jazzy_menu', 'closed');
} else {
setCookie('jazzy_menu', 'open');
}
});
}
function setActiveLinks() {
/*
Set the currently active menu item based on the current url, or failing that, find the parent
item from the breadcrumbs
*/
const url = window.location.pathname;
const $breadcrumb = $('.breadcrumb a').last();
const $link = $('a[href="' + url + '"]');
const $parent_link = $('a[href="' + $breadcrumb.attr('href') + '"]');
if ($link.length) {
$link.addClass('active');
} else if ($parent_link.length) {
$parent_link.addClass('active');
};
const $a_active = $('a.nav-link.active');
const $main_li_parent = $a_active.closest('li.nav-item.has-treeview');
const $ul_child = $main_li_parent.children('ul');
$ul_child.show();
$main_li_parent.addClass('menu-is-opening menu-open');
};
$(document).ready(function () {
// Set active status on links
setActiveLinks()
// When we use the menu, store its state in a cookie to preserve it
handleMenu();
// Add minimal changelist styling to templates that we have been unable to override (e.g MPTT)
// Needs to be here and not in change_list.js because this is the only JS we are guaranteed to run
// (as its included in base.html)
const $changeListTable = $('#changelist .results table');
if ($changeListTable.length && !$changeListTable.hasClass('table table-striped')) {
$changeListTable.addClass('table table-striped');
};
});
})(jQuery);

View File

@ -0,0 +1,188 @@
(function($) {
'use strict';
let relatedModalCounter = 0;
function checkIfInIframe() {
return window.top !== window.self;
}
// create the function that will close the modal
function dismissModal() {
if (checkIfInIframe()) {
const parentWindow = window.parent;
parentWindow.dismissModal();
return;
}
$('.related-modal-' + relatedModalCounter).modal('hide');
relatedModalCounter-=1;
}
// create the function that will show the modal
function showModal(title, body, e) {
if (checkIfInIframe()) {
const parentWindow = window.parent;
parentWindow.showModal(title, body, e);
return;
}
relatedModalCounter+=1;
$.showModal({
title: title,
body: body,
backdrop: false,
modalDialogClass: "modal-dialog-centered modal-lg",
modalClass: "fade modal-wide related-modal-" + relatedModalCounter,
onDispose: function() {
// add focus to the previous modal (if exists) when the current one is closed
var lastModal = $("div[class*='related-modal-']").last();
if (lastModal) {
lastModal.focus();
}
}
});
const modalEl = $("div[class*='related-modal-']");
const iframeEl = modalEl.find('#related-modal-iframe');
if (e.data.lookup === true) {
// set current window as iframe opener because
// the callback is called on the opener window
iframeEl.on('load', function() {
const iframeObj = $(this).get(0);
const iframeWindow = iframeObj.contentWindow;
iframeWindow.opener = window;
});
}
}
function dismissRelatedLookupModal(win, chosenId) {
const windowName = win.name;
const widgetName = windowName.replace(/^(change|add|delete|lookup)_/, '');
let widgetEl;
if (checkIfInIframe) {
// select second to last iframe in the main parent document
const secondLastIframe = $('iframe.related-iframe', win.parent.document).eq(-2);
let documentContext;
// if second to last iframe exists get its contents
if (secondLastIframe.length) {
documentContext = secondLastIframe.contents();
// else get main parent document
} else {
documentContext = $(win.parent.document);
}
// find and select widget from the specified document context
widgetEl = documentContext.find('#' + widgetName);
// else select widget from the main document
} else {
widgetEl = $('#' + widgetName);
}
const widgetVal = widgetEl.val();
if (widgetEl.hasClass('vManyToManyRawIdAdminField') && Boolean(widgetVal)) {
widgetEl.val(widgetVal + ', ' + chosenId);
} else {
widgetEl.val(chosenId);
}
dismissModal();
}
// assign functions to global variables
window.dismissRelatedObjectModal = dismissModal;
window.dismissRelatedLookupPopup = dismissRelatedLookupModal;
window.showModal = showModal;
function presentRelatedObjectModal(e) {
let linkEl = $(this);
let href = (linkEl.attr('href') || '');
if (href === '') {
return;
}
// open the popup as modal
e.preventDefault();
e.stopImmediatePropagation();
// remove focus from clicked link
linkEl.blur();
// use the clicked link id as iframe name
// it will be available as window.name in the loaded iframe
let iframeName = linkEl.attr('id');
let iframeSrc = href;
const modalTitle = linkEl.attr('title');
if (e.data.lookup !== true) {
// browsers stop loading nested iframes having the same src url
// create a random parameter and append it to the src url to prevent it
// this workaround doesn't work with related lookup url
let iframeSrcRandom = String(Math.round(Math.random() * 999999));
if (iframeSrc.indexOf('?') === -1) {
iframeSrc += '?_modal=' + iframeSrcRandom;
} else {
iframeSrc += '&_modal=' + iframeSrcRandom;
}
}
if (iframeSrc.indexOf('_popup=1') === -1) {
if (iframeSrc.indexOf('?') === -1) {
iframeSrc += '?_popup=1';
} else {
iframeSrc += '&_popup=1';
}
}
// build the iframe html
let iframeHTML = '<iframe id="related-modal-iframe" name="' + iframeName + '" src="' + iframeSrc + '" frameBorder="0" class="related-iframe"></iframe>';
// the modal css class
let iframeInternalModalClass = 'related-modal';
// if the current window is inside an iframe, it means that it is already in a modal,
// append an additional css class to the modal to offer more customization
if (window.top !== window.self) {
iframeInternalModalClass += ' related-modal__nested';
}
// open the modal using dynamic bootstrap modal
showModal(modalTitle, iframeHTML, e);
return false;
}
// listen click events on related links
function presentRelatedObjectModalOnClickOn(selector, lookup) {
let el = $(selector);
el.removeAttr('onclick');
el.unbind('click');
el.click({lookup: lookup}, presentRelatedObjectModal);
}
function init() {
presentRelatedObjectModalOnClickOn('a.related-widget-wrapper-link', false);
// raw_id_fields support
presentRelatedObjectModalOnClickOn('a.related-lookup', true);
// django-dynamic-raw-id support - #61
// https://github.com/lincolnloop/django-dynamic-raw-id
presentRelatedObjectModalOnClickOn('a.dynamic_raw_id-related-lookup', true);
}
$(document).ready(function(){
init()
});
django.jQuery(document).on('formset:added', init);
})(jQuery);

View File

@ -0,0 +1,343 @@
(function ($) {
'use strict';
const $body = $('body');
const $footer = $('footer');
const $sidebar_ul = $('aside#jazzy-sidebar nav ul:first-child');
const $sidebar = $('aside#jazzy-sidebar');
const $navbar = $('nav#jazzy-navbar');
const $logo = $('#jazzy-logo');
const $actions = $('#jazzy-actions');
const buttons = [
"primary",
"secondary",
"info",
"warning",
"danger",
"success",
]
const darkThemes = ["darkly", "cyborg", "slate", "solar", "superhero"]
window.ui_changes = window.ui_changes || {'button_classes': {}};
function miscListeners() {
$('#footer-fixed').on('click', function () {
$body.toggleClass('layout-footer-fixed');
if (this.checked) {
$('#layout-boxed:checked').click();
}
window.ui_changes['footer_fixed'] = this.checked;
});
$('#layout-boxed').on('click', function () {
$body.toggleClass('layout-boxed');
// We cannot combine these options with layout boxed
if (this.checked) {
$('#navbar-fixed:checked').click();
$('#footer-fixed:checked').click();
}
window.ui_changes['layout_boxed'] = this.checked;
});
$('#actions-fixed').on('click', function () {
$actions.toggleClass('sticky-top');
window.ui_changes['actions_sticky_top'] = this.checked;
});
// Colour pickers
$('#accent-colours div').on('click', function () {
$(this).removeClass('inactive').addClass('active').parent().find(
'div'
).not(this).removeClass('active').addClass('inactive');
const newClasses = $(this).data('classes');
$body.removeClass(function (index, className) {
return (className.match(/(^|\s)accent-\S+/g) || []).join(' ');
}).addClass(newClasses);
window.ui_changes['accent'] = newClasses;
});
$('#brand-logo-variants div').on('click', function () {
$(this).removeClass('inactive').addClass('active').parent().find(
'div'
).not(this).removeClass('active').addClass('inactive');
let newClasses = $(this).data('classes');
$logo.removeClass(function (index, className) {
return (className.match(/(^|\s)navbar-\S+/g) || []).join(' ');
}).addClass(newClasses);
if (newClasses === "") {
newClasses = false;
$(this).parent().find('div').removeClass('active inactive');
}
window.ui_changes['brand_colour'] = newClasses;
});
// show code
$("#codeBox").on('show.bs.modal', function () {
$('.modal-body code', this).html(
'JAZZMIN_UI_TWEAKS = ' + JSON.stringify(
window.ui_changes, null, 4
).replace(
/true/g, 'True'
).replace(
/false/g, 'False'
).replace(
/null/g, 'None'
)
);
});
}
function themeSpecificTweaks(theme) {
if (darkThemes.indexOf(theme) > -1) {
$('#navbar-variants .bg-dark').click();
$("#jazzmin-btn-style-primary").val('btn-primary').change();
$("#jazzmin-btn-style-secondary").val('btn-secondary').change();
$body.addClass('dark-mode');
} else {
$('#navbar-variants .bg-white').click();
$("#jazzmin-btn-style-primary").val('btn-outline-primary').change();
$("#jazzmin-btn-style-secondary").val('btn-outline-secondary').change();
$body.removeClass('dark-mode');
}
}
function themeChooserListeners() {
// Theme chooser (standard)
$("#jazzmin-theme-chooser").on('change', function () {
let $themeCSS = $('#jazzmin-theme');
// If we are using the default theme, there will be no theme css, just the bundled one in adminlte
if (!$themeCSS.length) {
const staticSrc = $('#adminlte-css').attr('href').split('vendor')[0]
$themeCSS = $('<link>').attr({
'href': staticSrc + 'vendor/bootswatch/default/bootstrap.min.css',
'rel': 'stylesheet',
'id': 'jazzmin-theme'
}).appendTo('head');
}
const currentSrc = $themeCSS.attr('href');
const currentTheme = currentSrc.split('/')[4];
let newTheme = $(this).val();
$themeCSS.attr('href', currentSrc.replace(currentTheme, newTheme));
$body.removeClass (function (index, className) {
return (className.match (/(^|\s)theme-\S+/g) || []).join(' ');
});
$body.addClass('theme-' + newTheme);
themeSpecificTweaks(newTheme);
window.ui_changes['theme'] = newTheme;
});
// Theme chooser (dark mode)
$("#jazzmin-dark-mode-theme-chooser").on('change', function () {
let $themeCSS = $('#jazzmin-dark-mode-theme');
// If we are using the default theme, there will be no theme css, just the bundled one in adminlte
if (this.value === "") {
$themeCSS.remove();
window.ui_changes['dark_mode_theme'] = null;
return
}
if (!$themeCSS.length) {
const staticSrc = $('#adminlte-css').attr('href').split('vendor')[0]
$themeCSS = $('<link>').attr({
'href': staticSrc + 'vendor/bootswatch/darkly/bootstrap.min.css',
'rel': 'stylesheet',
'id': 'jazzmin-dark-mode-theme',
'media': '(prefers-color-scheme: dark)'
}).appendTo('head');
}
const currentSrc = $themeCSS.attr('href');
const currentTheme = currentSrc.split('/')[4];
const newTheme = $(this).val();
$themeCSS.attr('href', currentSrc.replace(currentTheme, newTheme));
themeSpecificTweaks(newTheme);
window.ui_changes['dark_mode_theme'] = newTheme;
});
}
function navBarTweaksListeners() {
$('#navbar-fixed').on('click', function () {
$body.toggleClass('layout-navbar-fixed');
if (this.checked) {$('#layout-boxed:checked').click();}
window.ui_changes['navbar_fixed'] = this.checked;
});
$('#no-navbar-border').on('click', function () {
$navbar.toggleClass('border-bottom-0');
window.ui_changes['no_navbar_border'] = $navbar.hasClass('border-bottom-0');
});
// Colour picker
$('#navbar-variants div').on('click', function () {
$(this).removeClass('inactive').addClass('active').parent().find(
'div'
).not(this).removeClass('active').addClass('inactive');
const newClasses = $(this).data('classes');
$navbar.removeClass(function (index, className) {
return (className.match(/(^|\s)navbar-\S+/g) || []).join(' ');
}).addClass('navbar-expand ' + newClasses);
window.ui_changes['navbar'] = newClasses;
});
}
function sideBarTweaksListeners() {
$('#sidebar-nav-flat-style').on('click', function () {
$sidebar_ul.toggleClass('nav-flat');
window.ui_changes['sidebar_nav_flat_style'] = this.checked;
});
$('#sidebar-nav-legacy-style').on('click', function () {
$sidebar_ul.toggleClass('nav-legacy');
window.ui_changes['sidebar_nav_legacy_style'] = this.checked;
});
$('#sidebar-nav-compact').on('click', function () {
$sidebar_ul.toggleClass('nav-compact');
window.ui_changes['sidebar_nav_compact_style'] = this.checked;
});
$('#sidebar-nav-child-indent').on('click', function () {
$sidebar_ul.toggleClass('nav-child-indent');
window.ui_changes['sidebar_nav_child_indent'] = this.checked;
});
$('#main-sidebar-disable-hover-focus-auto-expand').on('click', function () {
$sidebar.toggleClass('sidebar-no-expand');
window.ui_changes['sidebar_disable_expand'] = this.checked;
});
$('#sidebar-fixed').on('click', function () {
$body.toggleClass('layout-fixed');
window.ui_changes['sidebar_fixed'] = this.checked;
});
// Colour pickers
$('#dark-sidebar-variants div, #light-sidebar-variants div').on('click', function () {
$(this).removeClass('inactive').addClass('active').parent().find(
'div'
).not(this).removeClass('active').addClass('inactive');
const newClasses = $(this).data('classes');
$sidebar.removeClass(function (index, className) {
return (className.match(/(^|\s)sidebar-[\S|-]+/g) || []).join(' ');
}).addClass(newClasses);
window.ui_changes['sidebar'] = newClasses.trim();
});
}
function smallTextListeners() {
$('#navbar-small-text').on('click', function () {
$navbar.toggleClass('text-sm');
window.ui_changes['navbar_small_text'] = this.checked;
});
$('#brand-small-text').on('click', function () {
$logo.toggleClass('text-sm');
window.ui_changes['brand_small_text'] = this.checked;
});
$('#body-small-text').on('click', function () {
$body.toggleClass('text-sm');
window.ui_changes['body_small_text'] = this.checked;
const $smallTextControls = $('#navbar-small-text, #brand-small-text, #footer-small-text, #sidebar-nav-small-text');
if (this.checked) {
window.ui_changes['navbar_small_text'] = false;
window.ui_changes['brand_small_text'] = false;
window.ui_changes['footer_small_text'] = false;
window.ui_changes['sidebar_nav_small_text'] = false;
$smallTextControls.prop({'checked': false, 'disabled': 'disabled'});
} else {
$smallTextControls.prop({'checked': false, 'disabled': ''});
}
});
$('#footer-small-text').on('click', function () {
$footer.toggleClass('text-sm');
window.ui_changes['footer_small_text'] = this.checked;
});
$('#sidebar-nav-small-text').on('click', function () {
$sidebar_ul.toggleClass('text-sm');
window.ui_changes['sidebar_nav_small_text'] = this.checked;
});
}
function buttonStyleListeners() {
buttons.forEach(function(btn) {
$("#jazzmin-btn-style-" + btn).on('change', function () {
const btnClasses = ['btn-' + btn, 'btn-outline-' + btn];
const selectorClasses = '.btn-' + btn + ', .btn-outline-' + btn;
$(selectorClasses).removeClass(btnClasses).addClass(this.value);
window.ui_changes['button_classes'][btn] = this.value;
});
});
}
function setFromExisting() {
$('#jazzmin-theme-chooser').val(window.ui_changes['theme']);
$('#jazzmin-dark-mode-theme-chooser').val(window.ui_changes['dark_mode_theme']);
$('#theme-condition').val(window.ui_changes['theme_condition']);
$('#body-small-text').get(0).checked = window.ui_changes['body_small_text'];
$('#footer-small-text').get(0).checked = window.ui_changes['footer_small_text'];
$('#sidebar-nav-small-text').get(0).checked = window.ui_changes['sidebar_nav_small_text'];
$('#sidebar-nav-legacy-style').get(0).checked = window.ui_changes['sidebar_nav_legacy_style'];
$('#sidebar-nav-compact').get(0).checked = window.ui_changes['sidebar_nav_compact_style'];
$('#sidebar-nav-child-indent').get(0).checked = window.ui_changes['sidebar_nav_child_indent'];
$('#main-sidebar-disable-hover-focus-auto-expand').get(0).checked = window.ui_changes['sidebar_disable_expand'];
$('#no-navbar-border').get(0).checked = window.ui_changes['no_navbar_border'];
$('#navbar-small-text').get(0).checked = window.ui_changes['navbar_small_text'];
$('#brand-small-text').get(0).checked = window.ui_changes['brand_small_text'];
// deactivate colours
$('#navbar-variants div, #accent-colours div, #dark-sidebar-variants div, #light-sidebar-variants div, #brand-logo-variants div').addClass('inactive');
// set button styles
buttons.forEach(function(btn) {
$("#jazzmin-btn-style-" + btn).val(window.ui_changes['button_classes'][btn]);
});
// set colours
$('#navbar-variants div[data-classes="' + window.ui_changes['navbar'] + '"]').addClass('active');
$('#accent-colours div[data-classes="' + window.ui_changes['accent'] + '"]').addClass('active');
$('#dark-sidebar-variants div[data-classes="' + window.ui_changes['sidebar'] + '"]').addClass('active');
$('#light-sidebar-variants div[data-classes="' + window.ui_changes['sidebar'] + '"]').addClass('active');
$('#brand-logo-variants div[data-classes="' + window.ui_changes['brand_colour'] + '"]').addClass('active');
}
/*
Don't call if it is inside an iframe
*/
if (!$body.hasClass("popup")) {
setFromExisting();
themeChooserListeners();
miscListeners();
navBarTweaksListeners();
sideBarTweaksListeners();
smallTextListeners();
buttonStyleListeners();
}
})(jQuery);

View File

@ -0,0 +1 @@
!function(o){"use strict";var s=0;function i(t){for(var e in this.props={title:"",body:"",footer:"",modalClass:"fade",modalDialogClass:"",options:null,onCreate:null,onDispose:null,onSubmit:null},t)this.props[e]=t[e];this.id="bootstrap-show-modal-"+s,s++,this.show()}i.prototype.createContainerElement=function(){var t=this;this.element=document.createElement("div"),this.element.id=this.id,this.element.setAttribute("class","modal "+this.props.modalClass),this.element.setAttribute("tabindex","-1"),this.element.setAttribute("role","dialog"),this.element.setAttribute("aria-labelledby",this.id),this.element.innerHTML='<div class="modal-dialog '+this.props.modalDialogClass+'" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"></h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"></div><div class="modal-footer"></div></div></div>',document.body.appendChild(this.element),this.titleElement=this.element.querySelector(".modal-title"),this.bodyElement=this.element.querySelector(".modal-body"),this.footerElement=this.element.querySelector(".modal-footer"),o(this.element).on("hidden.bs.modal",function(){t.dispose()}),this.props.onCreate&&this.props.onCreate(this)},i.prototype.show=function(){this.element?o(this.element).modal("show"):(this.createContainerElement(),this.props.options?o(this.element).modal(this.props.options):o(this.element).modal()),this.props.title?(o(this.titleElement).show(),this.titleElement.innerHTML=this.props.title):o(this.titleElement).hide(),this.props.body?(o(this.bodyElement).show(),this.bodyElement.innerHTML=this.props.body):o(this.bodyElement).hide(),this.props.footer?(o(this.footerElement).show(),this.footerElement.innerHTML=this.props.footer):o(this.footerElement).hide()},i.prototype.hide=function(){o(this.element).modal("hide")},i.prototype.dispose=function(){o(this.element).modal("dispose"),document.body.removeChild(this.element),this.props.onDispose&&this.props.onDispose(this)},o.extend({showModal:function(t){if(t.buttons){var e,o="";for(e in t.buttons){o+='<button type="button" class="btn btn-primary" data-value="'+e+'" data-dismiss="modal">'+t.buttons[e]+"</button>"}t.footer=o}return new i(t)},showAlert:function(t){return t.buttons={OK:"OK"},this.showModal(t)},showConfirm:function(t){return t.footer='<button class="btn btn-secondary btn-false btn-cancel">'+t.textFalse+'</button><button class="btn btn-primary btn-true">'+t.textTrue+"</button>",t.onCreate=function(e){o(e.element).on("click",".btn",function(t){t.preventDefault(),e.hide(),e.props.onSubmit(-1!==t.target.getAttribute("class").indexOf("btn-true"),e)})},this.showModal(t)}})}(jQuery);

118
staticfiles/js/chat.js Normal file
View File

@ -0,0 +1,118 @@
document.addEventListener('DOMContentLoaded', function() {
const chatWidget = document.getElementById('masar-chat-widget');
const chatToggle = document.getElementById('masar-chat-toggle');
const chatClose = document.getElementById('masar-chat-close');
const chatForm = document.getElementById('masar-chat-form');
const chatInput = document.getElementById('masar-chat-input');
const chatMessages = document.getElementById('masar-chat-messages');
if (!chatWidget) return;
// Toggle Chat
function toggleChat() {
if (chatWidget.classList.contains('d-none')) {
chatWidget.classList.remove('d-none');
setTimeout(() => chatInput.focus(), 100);
} else {
chatWidget.classList.add('d-none');
}
}
chatToggle.addEventListener('click', toggleChat);
chatClose.addEventListener('click', toggleChat);
// Send Message
chatForm.addEventListener('submit', function(e) {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
// Add User Message
addMessage(message, 'user');
chatInput.value = '';
// Show Typing Indicator
const typingId = addTypingIndicator();
// Send to Backend
fetch('/ajax/chatbot/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
message: message,
language: document.documentElement.lang || 'en'
})
})
.then(response => response.json())
.then(data => {
removeMessage(typingId);
if (data.success) {
addMessage(data.response, 'bot');
} else {
addMessage('Sorry, I encountered an error.', 'bot');
}
})
.catch(error => {
removeMessage(typingId);
addMessage('Sorry, connection error.', 'bot');
console.error('Error:', error);
});
});
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `d-flex mb-3 ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'}`;
const bubble = document.createElement('div');
bubble.className = `p-3 rounded-3 shadow-sm ${sender === 'user' ? 'bg-primary text-white' : 'bg-light text-dark'}`;
bubble.style.maxWidth = '80%';
bubble.style.wordWrap = 'break-word';
// Convert newlines to <br> for basic formatting
bubble.innerHTML = text.replace(/\n/g, '<br>');
div.appendChild(bubble);
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return div.id;
}
function addTypingIndicator() {
const id = 'typing-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = 'd-flex mb-3 justify-content-start';
div.innerHTML = ""
+ "<div class=\"bg-light p-3 rounded-3 shadow-sm\">"
+ " <div class=\"typing-dots\">"
+ " <span></span><span></span><span></span>"
+ " </div>"
+ "</div>"
;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return id;
}
function removeMessage(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long