676 lines
30 KiB
HTML
676 lines
30 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if object %}Edit System{% else %}New System{% endif %} - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
{% if object %}
|
|
<i class="fas fa-edit me-2"></i>Edit External System
|
|
{% else %}
|
|
<i class="fas fa-plus me-2"></i>New External System
|
|
{% endif %}
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'integration:dashboard' %}">Integration</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'integration:external_system_list' %}">External Systems</a></li>
|
|
{% if object %}
|
|
<li class="breadcrumb-item"><a href="{% url 'integration:external_system_detail' object.system_id %}">{{ object.name }}</a></li>
|
|
<li class="breadcrumb-item active">Edit</li>
|
|
{% else %}
|
|
<li class="breadcrumb-item active">New</li>
|
|
{% endif %}
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="{% if object %}{% url 'integration:external_system_detail' object.system_id %}{% else %}{% url 'integration:external_system_list' %}{% endif %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-2"></i>Back
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="post" novalidate>
|
|
{% csrf_token %}
|
|
|
|
<div class="row">
|
|
<!-- Main Form -->
|
|
<div class="col-lg-8">
|
|
<!-- Basic Information -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.name.id_for_label }}" class="form-label">
|
|
System Name <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.name }}
|
|
{% if form.name.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.name.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">Descriptive name for the external system</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.system_type.id_for_label }}" class="form-label">
|
|
System Type <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.system_type }}
|
|
{% if form.system_type.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.system_type.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
|
Description
|
|
</label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.description.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">Detailed description of the system and its purpose</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.vendor.id_for_label }}" class="form-label">
|
|
Vendor
|
|
</label>
|
|
{{ form.vendor }}
|
|
{% if form.vendor.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.vendor.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.version.id_for_label }}" class="form-label">
|
|
Version
|
|
</label>
|
|
{{ form.version }}
|
|
{% if form.version.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.version.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connection Details -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-plug me-2"></i>Connection Details
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label for="{{ form.base_url.id_for_label }}" class="form-label">
|
|
Base URL
|
|
</label>
|
|
{{ form.base_url }}
|
|
{% if form.base_url.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.base_url.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">Full URL for API-based systems (e.g., https://api.example.com)</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="mb-3">
|
|
<label for="{{ form.host.id_for_label }}" class="form-label">
|
|
Host
|
|
</label>
|
|
{{ form.host }}
|
|
{% if form.host.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.host.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">Hostname or IP address for direct connections</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label for="{{ form.port.id_for_label }}" class="form-label">
|
|
Port
|
|
</label>
|
|
{{ form.port }}
|
|
{% if form.port.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.port.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.database_name.id_for_label }}" class="form-label">
|
|
Database Name
|
|
</label>
|
|
{{ form.database_name }}
|
|
{% if form.database_name.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.database_name.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">Database name for database connections</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-lock me-2"></i>Authentication
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label for="{{ form.authentication_type.id_for_label }}" class="form-label">
|
|
Authentication Type <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.authentication_type }}
|
|
{% if form.authentication_type.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.authentication_type.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div id="auth-config-section">
|
|
<div class="mb-3">
|
|
<label for="auth_config" class="form-label">
|
|
Authentication Configuration
|
|
</label>
|
|
<textarea id="auth_config" name="authentication_config" class="form-control" rows="6"
|
|
placeholder='{"username": "your_username", "password": "your_password"}'
|
|
>{{ form.authentication_config.value|default:"" }}</textarea>
|
|
{% if form.authentication_config.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.authentication_config.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">JSON configuration for authentication parameters</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configuration -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-cogs me-2"></i>System Configuration
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label for="{{ form.timeout_seconds.id_for_label }}" class="form-label">
|
|
Timeout (seconds)
|
|
</label>
|
|
{{ form.timeout_seconds }}
|
|
{% if form.timeout_seconds.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.timeout_seconds.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label for="{{ form.retry_attempts.id_for_label }}" class="form-label">
|
|
Retry Attempts
|
|
</label>
|
|
{{ form.retry_attempts }}
|
|
{% if form.retry_attempts.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.retry_attempts.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label for="{{ form.retry_delay_seconds.id_for_label }}" class="form-label">
|
|
Retry Delay (seconds)
|
|
</label>
|
|
{{ form.retry_delay_seconds }}
|
|
{% if form.retry_delay_seconds.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.retry_delay_seconds.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="system_config" class="form-label">
|
|
System Configuration
|
|
</label>
|
|
<textarea id="system_config" name="configuration" class="form-control" rows="8"
|
|
placeholder='{"api_version": "v1", "format": "json", "compression": true}'
|
|
>{{ form.configuration.value|default:"" }}</textarea>
|
|
{% if form.configuration.errors %}
|
|
<div class="invalid-feedback d-block">
|
|
{{ form.configuration.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="form-text">JSON configuration for system-specific parameters</div>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="is_active" name="is_active"
|
|
{% if form.is_active.value %}checked{% endif %}>
|
|
<label class="form-check-label" for="is_active">
|
|
System is active and available for integrations
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Form Actions -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-save me-2"></i>Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save me-2"></i>
|
|
{% if object %}Update System{% else %}Create System{% endif %}
|
|
</button>
|
|
{% if object %}
|
|
<button type="button" class="btn btn-outline-info" onclick="testConnection()">
|
|
<i class="fas fa-plug me-2"></i>Test Connection
|
|
</button>
|
|
{% endif %}
|
|
<a href="{% if object %}{% url 'integration:external_system_detail' object.system_id %}{% else %}{% url 'integration:external_system_list' %}{% endif %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-times me-2"></i>Cancel
|
|
</a>
|
|
<button type="button" class="btn btn-outline-warning" onclick="clearForm()">
|
|
<i class="fas fa-eraser me-2"></i>Clear Form
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Type Guide -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-question-circle me-2"></i>System Types
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="accordion" id="systemTypeGuide">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#healthcareTypes">
|
|
Healthcare Systems
|
|
</button>
|
|
</h2>
|
|
<div id="healthcareTypes" class="accordion-collapse collapse" data-bs-parent="#systemTypeGuide">
|
|
<div class="accordion-body">
|
|
<ul class="list-unstyled mb-0">
|
|
<li><strong>EHR:</strong> Electronic Health Records</li>
|
|
<li><strong>HIS:</strong> Hospital Information System</li>
|
|
<li><strong>LIS:</strong> Laboratory Information System</li>
|
|
<li><strong>RIS:</strong> Radiology Information System</li>
|
|
<li><strong>PACS:</strong> Picture Archiving System</li>
|
|
<li><strong>Pharmacy:</strong> Pharmacy Management</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#technicalTypes">
|
|
Technical Systems
|
|
</button>
|
|
</h2>
|
|
<div id="technicalTypes" class="accordion-collapse collapse" data-bs-parent="#systemTypeGuide">
|
|
<div class="accordion-body">
|
|
<ul class="list-unstyled mb-0">
|
|
<li><strong>API:</strong> REST/SOAP API Services</li>
|
|
<li><strong>Database:</strong> Direct database connections</li>
|
|
<li><strong>File:</strong> File-based integrations</li>
|
|
<li><strong>FTP/SFTP:</strong> File transfer protocols</li>
|
|
<li><strong>Cloud:</strong> Cloud services</li>
|
|
<li><strong>IoT:</strong> Internet of Things devices</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication Guide -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-shield-alt me-2"></i>Authentication Guide
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="accordion" id="authGuide">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#basicAuth">
|
|
Basic Authentication
|
|
</button>
|
|
</h2>
|
|
<div id="basicAuth" class="accordion-collapse collapse" data-bs-parent="#authGuide">
|
|
<div class="accordion-body">
|
|
<code>
|
|
{<br>
|
|
"username": "your_username",<br>
|
|
"password": "your_password"<br>
|
|
}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#apiKeyAuth">
|
|
API Key
|
|
</button>
|
|
</h2>
|
|
<div id="apiKeyAuth" class="accordion-collapse collapse" data-bs-parent="#authGuide">
|
|
<div class="accordion-body">
|
|
<code>
|
|
{<br>
|
|
"api_key": "your_api_key",<br>
|
|
"header_name": "X-API-Key"<br>
|
|
}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#oauth2Auth">
|
|
OAuth 2.0
|
|
</button>
|
|
</h2>
|
|
<div id="oauth2Auth" class="accordion-collapse collapse" data-bs-parent="#authGuide">
|
|
<div class="accordion-body">
|
|
<code>
|
|
{<br>
|
|
"client_id": "your_client_id",<br>
|
|
"client_secret": "your_secret",<br>
|
|
"token_url": "https://auth.example.com/token",<br>
|
|
"scope": "read write"<br>
|
|
}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// External system form functionality
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Add Bootstrap classes to form elements
|
|
const form = document.querySelector('form');
|
|
const inputs = form.querySelectorAll('input, select, textarea');
|
|
|
|
inputs.forEach(input => {
|
|
if (!input.classList.contains('form-control') && !input.classList.contains('form-select') && !input.classList.contains('form-check-input')) {
|
|
if (input.tagName === 'SELECT') {
|
|
input.classList.add('form-select');
|
|
} else if (input.type !== 'checkbox') {
|
|
input.classList.add('form-control');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Authentication type change handler
|
|
const authTypeSelect = document.getElementById('id_authentication_type');
|
|
if (authTypeSelect) {
|
|
authTypeSelect.addEventListener('change', updateAuthConfigPlaceholder);
|
|
updateAuthConfigPlaceholder(); // Initial call
|
|
}
|
|
|
|
// JSON validation for configuration fields
|
|
const jsonFields = ['auth_config', 'system_config'];
|
|
jsonFields.forEach(fieldId => {
|
|
const field = document.getElementById(fieldId);
|
|
if (field) {
|
|
field.addEventListener('blur', function() {
|
|
validateJSON(this);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
function updateAuthConfigPlaceholder() {
|
|
const authType = document.getElementById('id_authentication_type').value;
|
|
const authConfig = document.getElementById('auth_config');
|
|
|
|
const placeholders = {
|
|
'basic': '{"username": "your_username", "password": "your_password"}',
|
|
'bearer': '{"token": "your_bearer_token"}',
|
|
'api_key': '{"api_key": "your_api_key", "header_name": "X-API-Key"}',
|
|
'oauth2': '{"client_id": "your_client_id", "client_secret": "your_secret", "token_url": "https://auth.example.com/token", "scope": "read write"}',
|
|
'certificate': '{"cert_file": "/path/to/cert.pem", "key_file": "/path/to/key.pem"}',
|
|
'custom': '{"custom_param": "value"}'
|
|
};
|
|
|
|
if (authConfig && placeholders[authType]) {
|
|
authConfig.placeholder = placeholders[authType];
|
|
}
|
|
}
|
|
|
|
function validateJSON(field) {
|
|
const value = field.value.trim();
|
|
if (value === '') {
|
|
field.classList.remove('is-invalid', 'is-valid');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
JSON.parse(value);
|
|
field.classList.remove('is-invalid');
|
|
field.classList.add('is-valid');
|
|
} catch (e) {
|
|
field.classList.remove('is-valid');
|
|
field.classList.add('is-invalid');
|
|
|
|
// Show error message
|
|
let errorDiv = field.parentNode.querySelector('.json-error');
|
|
if (!errorDiv) {
|
|
errorDiv = document.createElement('div');
|
|
errorDiv.className = 'invalid-feedback json-error';
|
|
field.parentNode.appendChild(errorDiv);
|
|
}
|
|
errorDiv.textContent = 'Invalid JSON format: ' + e.message;
|
|
}
|
|
}
|
|
|
|
function testConnection() {
|
|
const systemId = '{{ object.system_id }}';
|
|
|
|
fetch(`{% url 'integration:test_connection' 0 %}`.replace('0', systemId), {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Connection test successful!\n\nResponse time: ' + data.response_time + 'ms');
|
|
} else {
|
|
alert('Connection test failed:\n\n' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error testing connection:', error);
|
|
alert('Error testing connection: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function clearForm() {
|
|
if (confirm('Are you sure you want to clear all form data?')) {
|
|
const form = document.querySelector('form');
|
|
const inputs = form.querySelectorAll('input, select, textarea');
|
|
|
|
inputs.forEach(input => {
|
|
if (input.type !== 'hidden' && input.name !== 'csrfmiddlewaretoken') {
|
|
if (input.type === 'checkbox') {
|
|
input.checked = false;
|
|
} else {
|
|
input.value = '';
|
|
}
|
|
input.classList.remove('is-invalid', 'is-valid');
|
|
}
|
|
});
|
|
|
|
// Clear JSON error messages
|
|
const errorDivs = form.querySelectorAll('.json-error');
|
|
errorDivs.forEach(div => div.remove());
|
|
}
|
|
}
|
|
|
|
// Form submission with loading state
|
|
document.querySelector('form').addEventListener('submit', function() {
|
|
const submitBtn = document.querySelector('button[type="submit"]');
|
|
const originalText = submitBtn.innerHTML;
|
|
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing...';
|
|
submitBtn.disabled = true;
|
|
|
|
// Re-enable if form validation fails
|
|
setTimeout(() => {
|
|
if (document.querySelector('.is-invalid')) {
|
|
submitBtn.innerHTML = originalText;
|
|
submitBtn.disabled = false;
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
// Real-time validation
|
|
document.querySelectorAll('input[required], select[required]').forEach(input => {
|
|
input.addEventListener('blur', function() {
|
|
if (this.value.trim() === '') {
|
|
this.classList.add('is-invalid');
|
|
} else {
|
|
this.classList.remove('is-invalid');
|
|
}
|
|
});
|
|
|
|
input.addEventListener('input', function() {
|
|
if (this.classList.contains('is-invalid') && this.value.trim() !== '') {
|
|
this.classList.remove('is-invalid');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.is-invalid {
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
.is-valid {
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.invalid-feedback {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.875em;
|
|
color: #dc3545;
|
|
}
|
|
|
|
.text-danger {
|
|
color: #dc3545 !important;
|
|
}
|
|
|
|
.accordion-button:not(.collapsed) {
|
|
background-color: #e7f1ff;
|
|
color: #0d6efd;
|
|
}
|
|
|
|
.form-check-label {
|
|
cursor: pointer;
|
|
}
|
|
|
|
code {
|
|
background-color: #f8f9fa;
|
|
padding: 0.5rem;
|
|
border-radius: 0.25rem;
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.row.mb-3 .col-md-6,
|
|
.row.mb-3 .col-md-4,
|
|
.row.mb-3 .col-md-8 {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|