This commit is contained in:
Marwan Alwali 2025-09-01 11:26:11 +03:00
parent 1f0a6bff5f
commit 0a037d3d9d
51 changed files with 32377 additions and 2746 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@ -720,7 +720,7 @@ class InventoryStockUpdateView(LoginRequiredMixin, UpdateView):
template_name = 'inventory/stock/stock_form.html' template_name = 'inventory/stock/stock_form.html'
def get_queryset(self): def get_queryset(self):
return InventoryStock.objects.filter(tenant=self.request.user.tenant) return InventoryStock.objects.filter(inventory_item__tenant=self.request.user.tenant)
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()

View File

@ -493,10 +493,10 @@ class LabOrderDetailView(LoginRequiredMixin, DetailView):
lab_order = self.object lab_order = self.object
# Get specimens for this order # Get specimens for this order
context['specimens'] = lab_order.specimens.all().order_by('-collection_datetime') context['specimens'] = lab_order.specimens.all().order_by('-collected_datetime')
# Get results for this order # Get results for this order
context['results'] = lab_order.results.all().order_by('-result_datetime') context['results'] = lab_order.results.all().order_by('-created_at')
return context return context

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-08-31 19:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quality", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="qualityindicator",
name="current_value",
field=models.DecimalField(decimal_places=2, default=100, max_digits=10),
preserve_default=False,
),
]

View File

@ -43,6 +43,7 @@ class QualityIndicator(models.Model):
type = models.CharField(max_length=20, choices=TYPE_CHOICES) type = models.CharField(max_length=20, choices=TYPE_CHOICES)
measurement_unit = models.CharField(max_length=50) measurement_unit = models.CharField(max_length=50)
target_value = models.DecimalField(max_digits=10, decimal_places=2) target_value = models.DecimalField(max_digits=10, decimal_places=2)
current_value = models.DecimalField(max_digits=10, decimal_places=2)
threshold_warning = models.DecimalField(max_digits=10, decimal_places=2) threshold_warning = models.DecimalField(max_digits=10, decimal_places=2)
threshold_critical = models.DecimalField(max_digits=10, decimal_places=2) threshold_critical = models.DecimalField(max_digits=10, decimal_places=2)
calculation_method = models.TextField() calculation_method = models.TextField()

View File

@ -43,10 +43,10 @@ urlpatterns = [
# ============================================================================ # ============================================================================
# QUALITY MEASUREMENT URLS (LIMITED CRUD - Operational Data) # QUALITY MEASUREMENT URLS (LIMITED CRUD - Operational Data)
# ============================================================================ # ============================================================================
path('measurements/', views.QualityMeasurementListView.as_view(), name='quality_measurement_list'), path('measurements/', views.QualityMeasurementListView.as_view(), name='measurement_list'),
path('measurements/create/', views.QualityMeasurementCreateView.as_view(), name='quality_measurement_create'), path('measurements/create/', views.QualityMeasurementCreateView.as_view(), name='measurement_create'),
path('measurements/<int:pk>/', views.QualityMeasurementDetailView.as_view(), name='quality_measurement_detail'), path('measurements/<int:pk>/', views.QualityMeasurementDetailView.as_view(), name='measurement_detail'),
path('measurements/<int:pk>/update/', views.QualityMeasurementUpdateView.as_view(), name='quality_measurement_update'), path('measurements/<int:pk>/update/', views.QualityMeasurementUpdateView.as_view(), name='measurement_update'),
# Note: No delete view for measurements - operational tracking data # Note: No delete view for measurements - operational tracking data
# ============================================================================ # ============================================================================

View File

@ -105,13 +105,13 @@ class QualityDashboardView(LoginRequiredMixin, TemplateView):
# Recent measurements # Recent measurements
context['recent_measurements'] = QualityMeasurement.objects.filter( context['recent_measurements'] = QualityMeasurement.objects.filter(
tenant=tenant tenant=tenant
).select_related('indicator', 'measured_by').order_by('-measurement_date')[:10] ).select_related('indicator', 'created_by').order_by('-measurement_date')[:10]
# Active improvement projects # Active improvement projects
context['active_projects'] = ImprovementProject.objects.filter( context['active_projects'] = ImprovementProject.objects.filter(
tenant=tenant, tenant=tenant,
status='IN_PROGRESS' status='IN_PROGRESS'
).select_related('project_manager').order_by('-start_date')[:5] ).select_related('project_manager').order_by('-actual_start_date')[:5]
# Quality indicators performance # Quality indicators performance
context['indicator_performance'] = QualityIndicator.objects.filter( context['indicator_performance'] = QualityIndicator.objects.filter(
@ -137,7 +137,7 @@ class QualityIndicatorListView(LoginRequiredMixin, ListView):
List all quality indicators with filtering and search. List all quality indicators with filtering and search.
""" """
model = QualityIndicator model = QualityIndicator
template_name = 'quality/quality_indicator_list.html' template_name = 'quality/indicators/quality_indicator_list.html'
context_object_name = 'quality_indicators' context_object_name = 'quality_indicators'
paginate_by = 25 paginate_by = 25
@ -175,13 +175,13 @@ class QualityIndicatorListView(LoginRequiredMixin, ListView):
elif performance == 'below_target': elif performance == 'below_target':
queryset = queryset.filter(current_value__lt=F('target_value')) queryset = queryset.filter(current_value__lt=F('target_value'))
return queryset.order_by('category', 'indicator_name') return queryset.order_by('category', 'name')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'categories': QualityIndicator._meta.get_field('category').choices, 'categories': QualityIndicator._meta.get_field('category').choices,
'frequencies': QualityIndicator._meta.get_field('measurement_frequency').choices, 'frequencies': QualityIndicator._meta.get_field('frequency').choices,
'search_query': self.request.GET.get('search', ''), 'search_query': self.request.GET.get('search', ''),
}) })
return context return context
@ -192,7 +192,7 @@ class QualityIndicatorDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a quality indicator. Display detailed information about a quality indicator.
""" """
model = QualityIndicator model = QualityIndicator
template_name = 'quality/quality_indicator_detail.html' template_name = 'quality/indicators/quality_indicator_detail.html'
context_object_name = 'quality_indicator' context_object_name = 'quality_indicator'
def get_queryset(self): def get_queryset(self):
@ -252,7 +252,7 @@ class QualityIndicatorCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cr
""" """
model = QualityIndicator model = QualityIndicator
form_class = QualityIndicatorForm form_class = QualityIndicatorForm
template_name = 'quality/quality_indicator_form.html' template_name = 'quality/indicators/quality_indicator_form.html'
permission_required = 'quality.add_qualityindicator' permission_required = 'quality.add_qualityindicator'
success_url = reverse_lazy('quality:quality_indicator_list') success_url = reverse_lazy('quality:quality_indicator_list')
@ -284,7 +284,7 @@ class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
""" """
model = QualityIndicator model = QualityIndicator
form_class = QualityIndicatorForm form_class = QualityIndicatorForm
template_name = 'quality/quality_indicator_form.html' template_name = 'quality/indicators/quality_indicator_form.html'
permission_required = 'quality.change_qualityindicator' permission_required = 'quality.change_qualityindicator'
def get_queryset(self): def get_queryset(self):
@ -297,7 +297,7 @@ class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='QUALITY_INDICATOR_UPDATED', action='QUALITY_INDICATOR_UPDATED',
model='QualityIndicator', model='QualityIndicator',
@ -317,7 +317,7 @@ class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
Delete a quality indicator (soft delete by deactivating). Delete a quality indicator (soft delete by deactivating).
""" """
model = QualityIndicator model = QualityIndicator
template_name = 'quality/quality_indicator_confirm_delete.html' template_name = 'quality/indicators/quality_indicator_confirm_delete.html'
permission_required = 'quality.delete_qualityindicator' permission_required = 'quality.delete_qualityindicator'
success_url = reverse_lazy('quality:quality_indicator_list') success_url = reverse_lazy('quality:quality_indicator_list')
@ -341,7 +341,7 @@ class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
self.object.save() self.object.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='QUALITY_INDICATOR_DEACTIVATED', action='QUALITY_INDICATOR_DEACTIVATED',
model='QualityIndicator', model='QualityIndicator',
@ -362,7 +362,7 @@ class ImprovementProjectListView(LoginRequiredMixin, ListView):
List all improvement projects with filtering and search. List all improvement projects with filtering and search.
""" """
model = ImprovementProject model = ImprovementProject
template_name = 'quality/improvement_project_list.html' template_name = 'quality/projects/project_list.html'
context_object_name = 'improvement_projects' context_object_name = 'improvement_projects'
paginate_by = 25 paginate_by = 25
@ -393,7 +393,7 @@ class ImprovementProjectListView(LoginRequiredMixin, ListView):
if manager_id: if manager_id:
queryset = queryset.filter(project_manager_id=manager_id) queryset = queryset.filter(project_manager_id=manager_id)
return queryset.select_related('project_manager').order_by('-start_date') return queryset.select_related('project_manager').order_by('-actual_start_date')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -410,7 +410,7 @@ class ImprovementProjectDetailView(LoginRequiredMixin, DetailView):
Display detailed information about an improvement project. Display detailed information about an improvement project.
""" """
model = ImprovementProject model = ImprovementProject
template_name = 'quality/improvement_project_detail.html' template_name = 'quality/projects/project_detail.html'
context_object_name = 'improvement_project' context_object_name = 'improvement_project'
def get_queryset(self): def get_queryset(self):
@ -445,7 +445,7 @@ class ImprovementProjectCreateView(LoginRequiredMixin, PermissionRequiredMixin,
""" """
model = ImprovementProject model = ImprovementProject
form_class = ImprovementProjectForm form_class = ImprovementProjectForm
template_name = 'quality/improvement_project_form.html' template_name = 'quality/projects/project_form.html'
permission_required = 'quality.add_improvementproject' permission_required = 'quality.add_improvementproject'
success_url = reverse_lazy('quality:improvement_project_list') success_url = reverse_lazy('quality:improvement_project_list')
@ -476,7 +476,7 @@ class ImprovementProjectUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
""" """
model = ImprovementProject model = ImprovementProject
form_class = ImprovementProjectForm form_class = ImprovementProjectForm
template_name = 'quality/improvement_project_form.html' template_name = 'quality/projects/project_form.html'
permission_required = 'quality.change_improvementproject' permission_required = 'quality.change_improvementproject'
def get_queryset(self): def get_queryset(self):
@ -509,7 +509,7 @@ class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
Delete an improvement project. Delete an improvement project.
""" """
model = ImprovementProject model = ImprovementProject
template_name = 'quality/improvement_project_confirm_delete.html' template_name = 'quality/projects/project_confirm_delete.html'
permission_required = 'quality.delete_improvementproject' permission_required = 'quality.delete_improvementproject'
success_url = reverse_lazy('quality:improvement_project_list') success_url = reverse_lazy('quality:improvement_project_list')
@ -521,7 +521,7 @@ class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
project_name = self.object.project_name project_name = self.object.project_name
# Log the action before deletion # Log the action before deletion
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='IMPROVEMENT_PROJECT_DELETED', action='IMPROVEMENT_PROJECT_DELETED',
model='ImprovementProject', model='ImprovementProject',
@ -543,7 +543,7 @@ class AuditPlanListView(LoginRequiredMixin, ListView):
List all audit plans with filtering and search. List all audit plans with filtering and search.
""" """
model = AuditPlan model = AuditPlan
template_name = 'quality/audit_plan_list.html' template_name = 'quality/audits/audit_list.html'
context_object_name = 'audit_plans' context_object_name = 'audit_plans'
paginate_by = 25 paginate_by = 25
@ -594,7 +594,7 @@ class AuditPlanDetailView(LoginRequiredMixin, DetailView):
Display detailed information about an audit plan. Display detailed information about an audit plan.
""" """
model = AuditPlan model = AuditPlan
template_name = 'quality/audit_plan_detail.html' template_name = 'quality/audits/audit_detail.html'
context_object_name = 'audit_plan' context_object_name = 'audit_plan'
def get_queryset(self): def get_queryset(self):
@ -628,7 +628,7 @@ class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVie
""" """
model = AuditPlan model = AuditPlan
form_class = AuditPlanForm form_class = AuditPlanForm
template_name = 'quality/audit_plan_form.html' template_name = 'quality/audits/audit_form.html'
permission_required = 'quality.add_auditplan' permission_required = 'quality.add_auditplan'
success_url = reverse_lazy('quality:audit_plan_list') success_url = reverse_lazy('quality:audit_plan_list')
@ -637,7 +637,7 @@ class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVie
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='AUDIT_PLAN_CREATED', action='AUDIT_PLAN_CREATED',
model='AuditPlan', model='AuditPlan',
@ -658,7 +658,7 @@ class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
Update an audit plan (limited to status and notes after start). Update an audit plan (limited to status and notes after start).
""" """
model = AuditPlan model = AuditPlan
template_name = 'quality/audit_plan_update_form.html' template_name = 'quality/audits/audit_form.html'
permission_required = 'quality.change_auditplan' permission_required = 'quality.change_auditplan'
def get_queryset(self): def get_queryset(self):
@ -682,7 +682,7 @@ class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='AUDIT_PLAN_UPDATED', action='AUDIT_PLAN_UPDATED',
model='AuditPlan', model='AuditPlan',
@ -706,7 +706,7 @@ class QualityMeasurementListView(LoginRequiredMixin, ListView):
List all quality measurements with filtering and search. List all quality measurements with filtering and search.
""" """
model = QualityMeasurement model = QualityMeasurement
template_name = 'quality/quality_measurement_list.html' template_name = 'quality/measurements/measurement_list.html'
context_object_name = 'quality_measurements' context_object_name = 'quality_measurements'
paginate_by = 25 paginate_by = 25
@ -741,7 +741,7 @@ class QualityMeasurementListView(LoginRequiredMixin, ListView):
'indicators': QualityIndicator.objects.filter( 'indicators': QualityIndicator.objects.filter(
tenant=self.request.user.tenant, tenant=self.request.user.tenant,
is_active=True is_active=True
).order_by('indicator_name'), ).order_by('name'),
'statuses': QualityMeasurement._meta.get_field('status').choices, 'statuses': QualityMeasurement._meta.get_field('status').choices,
}) })
return context return context
@ -752,7 +752,7 @@ class QualityMeasurementDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a quality measurement. Display detailed information about a quality measurement.
""" """
model = QualityMeasurement model = QualityMeasurement
template_name = 'quality/quality_measurement_detail.html' template_name = 'quality/measurements/measurement_detail.html'
context_object_name = 'quality_measurement' context_object_name = 'quality_measurement'
def get_queryset(self): def get_queryset(self):
@ -765,7 +765,7 @@ class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin,
""" """
model = QualityMeasurement model = QualityMeasurement
form_class = QualityMeasurementForm form_class = QualityMeasurementForm
template_name = 'quality/quality_measurement_form.html' template_name = 'quality/measurements/measurement_form.html'
permission_required = 'quality.add_qualitymeasurement' permission_required = 'quality.add_qualitymeasurement'
success_url = reverse_lazy('quality:quality_measurement_list') success_url = reverse_lazy('quality:quality_measurement_list')
@ -781,7 +781,7 @@ class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin,
indicator.save() indicator.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='QUALITY_MEASUREMENT_CREATED', action='QUALITY_MEASUREMENT_CREATED',
model='QualityMeasurement', model='QualityMeasurement',
@ -803,7 +803,7 @@ class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
""" """
model = QualityMeasurement model = QualityMeasurement
fields = ['notes', 'status'] # Restricted fields fields = ['notes', 'status'] # Restricted fields
template_name = 'quality/quality_measurement_update_form.html' template_name = 'quality/measurements/measurement_form.html'
permission_required = 'quality.change_qualitymeasurement' permission_required = 'quality.change_qualitymeasurement'
def get_queryset(self): def get_queryset(self):
@ -821,7 +821,7 @@ class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='QUALITY_MEASUREMENT_UPDATED', action='QUALITY_MEASUREMENT_UPDATED',
model='QualityMeasurement', model='QualityMeasurement',
@ -845,7 +845,7 @@ class RiskAssessmentListView(LoginRequiredMixin, ListView):
List all risk assessments with filtering and search. List all risk assessments with filtering and search.
""" """
model = RiskAssessment model = RiskAssessment
template_name = 'quality/risk_assessment_list.html' template_name = 'quality/risk_assessments/risk_assessment_list.html'
context_object_name = 'risk_assessments' context_object_name = 'risk_assessments'
paginate_by = 25 paginate_by = 25
@ -893,7 +893,7 @@ class RiskAssessmentDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a risk assessment. Display detailed information about a risk assessment.
""" """
model = RiskAssessment model = RiskAssessment
template_name = 'quality/risk_assessment_detail.html' template_name = 'quality/risk_assessments/risk_assessment_detail.html'
context_object_name = 'risk_assessment' context_object_name = 'risk_assessment'
def get_queryset(self): def get_queryset(self):
@ -906,7 +906,7 @@ class RiskAssessmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
""" """
model = RiskAssessment model = RiskAssessment
form_class = RiskAssessmentForm form_class = RiskAssessmentForm
template_name = 'quality/risk_assessment_form.html' template_name = 'quality/risk_assessments/risk_assessment_form.html'
permission_required = 'quality.add_riskassessment' permission_required = 'quality.add_riskassessment'
success_url = reverse_lazy('quality:risk_assessment_list') success_url = reverse_lazy('quality:risk_assessment_list')
@ -937,7 +937,7 @@ class RiskAssessmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
Update risk assessment (limited after approval). Update risk assessment (limited after approval).
""" """
model = RiskAssessment model = RiskAssessment
template_name = 'quality/risk_assessment_update_form.html' template_name = 'quality/risk_assessments/risk_assessment_form.html'
permission_required = 'quality.change_riskassessment' permission_required = 'quality.change_riskassessment'
def get_queryset(self): def get_queryset(self):
@ -985,7 +985,7 @@ class IncidentReportListView(LoginRequiredMixin, ListView):
List all incident reports with filtering and search. List all incident reports with filtering and search.
""" """
model = IncidentReport model = IncidentReport
template_name = 'quality/incident_report_list.html' template_name = 'quality/incident_reports/incident_report_list.html'
context_object_name = 'incident_reports' context_object_name = 'incident_reports'
paginate_by = 25 paginate_by = 25
@ -1042,7 +1042,7 @@ class IncidentReportDetailView(LoginRequiredMixin, DetailView):
Display detailed information about an incident report. Display detailed information about an incident report.
""" """
model = IncidentReport model = IncidentReport
template_name = 'quality/incident_report_detail.html' template_name = 'quality/incident_reports/incident_report_detail.html'
context_object_name = 'incident_report' context_object_name = 'incident_report'
def get_queryset(self): def get_queryset(self):
@ -1055,7 +1055,7 @@ class IncidentReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
""" """
model = IncidentReport model = IncidentReport
form_class = IncidentReportForm form_class = IncidentReportForm
template_name = 'quality/incident_report_form.html' template_name = 'quality/incident_reports/incident_report_form.html'
permission_required = 'quality.add_incidentreport' permission_required = 'quality.add_incidentreport'
success_url = reverse_lazy('quality:incident_report_list') success_url = reverse_lazy('quality:incident_report_list')
@ -1093,7 +1093,7 @@ class AuditFindingListView(LoginRequiredMixin, ListView):
List all audit findings with filtering and search. List all audit findings with filtering and search.
""" """
model = AuditFinding model = AuditFinding
template_name = 'quality/audit_finding_list.html' template_name = 'quality/findings/finding_list.html'
context_object_name = 'audit_findings' context_object_name = 'audit_findings'
paginate_by = 25 paginate_by = 25
@ -1144,7 +1144,7 @@ class AuditFindingDetailView(LoginRequiredMixin, DetailView):
Display detailed information about an audit finding. Display detailed information about an audit finding.
""" """
model = AuditFinding model = AuditFinding
template_name = 'quality/audit_finding_detail.html' template_name = 'quality/findings/finding_detail.html'
context_object_name = 'audit_finding' context_object_name = 'audit_finding'
def get_queryset(self): def get_queryset(self):
@ -1157,7 +1157,7 @@ class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
""" """
model = AuditFinding model = AuditFinding
form_class = AuditFindingForm form_class = AuditFindingForm
template_name = 'quality/audit_finding_form.html' template_name = 'quality/findings/finding_form.html'
permission_required = 'quality.add_auditfinding' permission_required = 'quality.add_auditfinding'
success_url = reverse_lazy('quality:audit_finding_list') success_url = reverse_lazy('quality:audit_finding_list')
@ -1167,7 +1167,7 @@ class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='AUDIT_FINDING_CREATED', action='AUDIT_FINDING_CREATED',
model='AuditFinding', model='AuditFinding',
@ -1279,7 +1279,7 @@ def approve_risk_assessment(request, assessment_id):
assessment.save() assessment.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='RISK_ASSESSMENT_APPROVED', action='RISK_ASSESSMENT_APPROVED',
model='RiskAssessment', model='RiskAssessment',

View File

@ -139,7 +139,7 @@ class ReportTemplateListView(LoginRequiredMixin, ListView):
if active_only: if active_only:
queryset = queryset.filter(is_active=True) queryset = queryset.filter(is_active=True)
return queryset.order_by('template_name') return queryset.order_by('name')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -167,8 +167,8 @@ class ReportTemplateDetailView(LoginRequiredMixin, DetailView):
# Get recent reports using this template # Get recent reports using this template
context['recent_reports'] = RadiologyReport.objects.filter( context['recent_reports'] = RadiologyReport.objects.filter(
template=template, template_used=template,
tenant=self.request.user.tenant study__tenant=self.request.user.tenant
).select_related('study__order__patient').order_by('-created_at')[:10] ).select_related('study__order__patient').order_by('-created_at')[:10]
return context return context
@ -190,7 +190,7 @@ class ReportTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='REPORT_TEMPLATE_CREATED', action='REPORT_TEMPLATE_CREATED',
model='ReportTemplate', model='ReportTemplate',
@ -224,7 +224,7 @@ class ReportTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='REPORT_TEMPLATE_UPDATED', action='REPORT_TEMPLATE_UPDATED',
model='ReportTemplate', model='ReportTemplate',
@ -687,12 +687,12 @@ class DICOMImageListView(LoginRequiredMixin, ListView):
List all DICOM images with filtering and search. List all DICOM images with filtering and search.
""" """
model = DICOMImage model = DICOMImage
template_name = 'radiology/dicom_image_list.html' template_name = 'radiology/dicom/dicom_file_list.html'
context_object_name = 'dicom_images' context_object_name = 'dicom_images'
paginate_by = 50 paginate_by = 50
def get_queryset(self): def get_queryset(self):
queryset = DICOMImage.objects.filter(tenant=self.request.user.tenant) queryset = DICOMImage.objects.filter(series__study__tenant=self.request.user.tenant)
# Filter by series # Filter by series
series_id = self.request.GET.get('series') series_id = self.request.GET.get('series')
@ -710,7 +710,7 @@ class DICOMImageListView(LoginRequiredMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'series': ImagingSeries.objects.filter( 'series': ImagingSeries.objects.filter(
tenant=self.request.user.tenant study__tenant=self.request.user.tenant
).select_related('study__order__patient').order_by('-created_at')[:50], ).select_related('study__order__patient').order_by('-created_at')[:50],
}) })
return context return context

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -189,18 +189,18 @@
<div class="row"> <div class="row">
<!-- Recent Purchase Orders --> <!-- Recent Purchase Orders -->
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card h-100"> <div class="panel panel-inverse" data-sortable-id="index-1">
<div class="card-header"> <div class="panel-heading">
<div class="d-flex justify-content-between align-items-center"> <h4 class="panel-title"><i class="fas fa-shopping-cart me-2"></i>Recent Purchase Orders</h4>
<h5 class="mb-0"> <div class="panel-heading-btn">
<i class="fas fa-shopping-cart me-2"></i>Recent Purchase Orders <a href="{% url 'inventory:purchase_order_list' %}" class="btn btn-outline-primary btn-xs"><i class="fas fa-list me-1"></i>View All</a>
</h5> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="{% url 'inventory:purchase_order_list' %}" class="btn btn-outline-primary btn-sm"> <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<i class="fas fa-list me-1"></i>View All <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</a> <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div> </div>
</div> </div>
<div class="card-body p-0"> <div class="panel-body">
{% if recent_orders %} {% if recent_orders %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
@ -228,8 +228,8 @@
{{ order.get_status_display }} {{ order.get_status_display }}
</span> </span>
</td> </td>
<td>${{ order.total_amount|floatformat:2 }}</td> <td><span class="symbol">&#xea;</span>{{ order.total_amount|floatformat:'2g' }}</td>
<td>{{ order.order_date|date:"M d" }}</td> <td>{{ order.order_date|date:" Y M d" }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -251,18 +251,18 @@
<!-- Low Stock Alerts --> <!-- Low Stock Alerts -->
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card h-100"> <div class="panel panel-inverse" data-sortable-id="index-2">
<div class="card-header"> <div class="panel-heading">
<div class="d-flex justify-content-between align-items-center"> <h4 class="panel-title"><i class="fas fa-exclamation-triangle me-2"></i>Low Stock Alerts</h4>
<h5 class="mb-0"> <div class="panel-heading-btn">
<i class="fas fa-exclamation-triangle me-2"></i>Low Stock Alerts <a href="{% url 'inventory:stock_list' %}?stock_status=low" class="btn btn-outline-warning btn-xs"><i class="fas fa-list me-1"></i>View All</a>
</h5> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="{% url 'inventory:stock_list' %}?stock_status=low" class="btn btn-outline-warning btn-sm"> <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<i class="fas fa-list me-1"></i>View All <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</a> <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div> </div>
</div> </div>
<div class="card-body"> <div class="panel-body">
{% if low_stock_alerts %} {% if low_stock_alerts %}
{% for alert in low_stock_alerts %} {% for alert in low_stock_alerts %}
<div class="d-flex align-items-center mb-3 p-3 bg-light rounded"> <div class="d-flex align-items-center mb-3 p-3 bg-light rounded">
@ -294,18 +294,18 @@
<div class="row"> <div class="row">
<!-- Recent Stock Movements --> <!-- Recent Stock Movements -->
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card h-100"> <div class="panel panel-inverse" data-sortable-id="index-3">
<div class="card-header"> <div class="panel-heading">
<div class="d-flex justify-content-between align-items-center"> <h4 class="panel-title"><i class="fas fa-exchange-alt me-2"></i>Recent Stock Movements</h4>
<h5 class="mb-0"> <div class="panel-heading-btn">
<i class="fas fa-exchange-alt me-2"></i>Recent Stock Movements <a href="{% url 'inventory:stock_list' %}" class="btn btn-outline-primary btn-xs"><i class="fas fa-list me-1"></i>View All</a>
</h5> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="{% url 'inventory:stock_list' %}" class="btn btn-outline-primary btn-sm"> <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<i class="fas fa-list me-1"></i>View All <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</a> <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div> </div>
</div> </div>
<div class="card-body p-0"> <div class="panel-body">
{% if recent_stock_movements %} {% if recent_stock_movements %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
@ -351,13 +351,17 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card h-100"> <div class="panel panel-inverse" data-sortable-id="index-4">
<div class="card-header"> <div class="panel-heading">
<h5 class="mb-0"> <h4 class="panel-title"><i class="fas fa-bolt me-2"></i>Quick Actions</h4>
<i class="fas fa-bolt me-2"></i>Quick Actions <div class="panel-heading-btn">
</h5> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div> </div>
<div class="card-body"> <div class="panel-body">
<div class="row g-3"> <div class="row g-3">
<div class="col-6"> <div class="col-6">
<a href="{% url 'inventory:item_list' %}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column align-items-center justify-content-center"> <a href="{% url 'inventory:item_list' %}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column align-items-center justify-content-center">
@ -404,33 +408,44 @@
<!-- Inventory Charts --> <!-- Inventory Charts -->
<div class="row"> <div class="row">
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-5">
<div class="card-header"> <div class="panel-heading">
<h5 class="mb-0"> <h4 class="panel-title">
<i class="fas fa-chart-pie me-2"></i>Inventory by Category <i class="fas fa-chart-pie me-2"></i>Inventory by Category
</h5> </h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div> </div>
<div class="card-body"> <div class="panel-body">
<canvas id="categoryChart" width="400" height="200"></canvas> <canvas id="categoryChart" width="400" height="200"></canvas>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-6">
<div class="card-header"> <div class="panel-heading">
<h5 class="mb-0"> <h4 class="panel-title">
<i class="fas fa-chart-bar me-2"></i>Stock Status Overview <i class="fas fa-chart-bar me-2"></i>Stock Status Overview
</h5> </h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div> </div>
<div class="card-body"> <div class="panel-body">
<canvas id="stockStatusChart" width="400" height="200"></canvas> <canvas id="stockStatusChart" width="400" height="200"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Dashboard functionality // Dashboard functionality
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@ -467,9 +482,9 @@ function initializeCharts() {
new Chart(categoryCtx, { new Chart(categoryCtx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: {{ category_labels|safe }}, labels: ['blue', 'green', 'yellow', 'red', 'purple', 'orange', 'pink'],
datasets: [{ datasets: [{
data: {{ category_data|safe }}, data: [16, 12, 14, 10, 18, 20, 4],
backgroundColor: [ backgroundColor: [
'#0d6efd', '#198754', '#ffc107', '#dc3545', '#0d6efd', '#198754', '#ffc107', '#dc3545',
'#6f42c1', '#fd7e14', '#20c997', '#6c757d' '#6f42c1', '#fd7e14', '#20c997', '#6c757d'
@ -497,7 +512,7 @@ function initializeCharts() {
labels: ['In Stock', 'Low Stock', 'Out of Stock', 'Expired'], labels: ['In Stock', 'Low Stock', 'Out of Stock', 'Expired'],
datasets: [{ datasets: [{
data: [ data: [
{{ in_stock_count }}, {{ total_items }},
{{ low_stock_items }}, {{ low_stock_items }},
{{ out_of_stock_count }}, {{ out_of_stock_count }},
{{ expired_items }} {{ expired_items }}

View File

@ -34,18 +34,20 @@
<!-- Location Overview --> <!-- Location Overview -->
<div class="row"> <div class="row">
<div class="col-lg-8 col-sm-12"> <div class="col-lg-8 col-sm-12">
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-1">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Location Information</h5> <h4 class="panel-title">Location Information</h4>
<div class="card-tools"> <div class="panel-heading-btn">
{% if location.is_active %} {% if location.is_active %}
<span class="badge bg-success">Active</span> <span class="badge bg-success me-2">Active</span>
{% else %} {% else %}
<span class="badge bg-danger">Inactive</span> <span class="badge bg-danger me-2">Inactive</span>
{% endif %} {% endif %}
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div> </div>
</div> </div>
<div class="card-body"> <div class="panel-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="info-group"> <div class="info-group">
@ -127,11 +129,15 @@
</div> </div>
<!-- Environmental Controls --> <!-- Environmental Controls -->
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-2">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Environmental Controls & Capacity</h5> <h4 class="panel-title">Environmental Controls & Capacity</h4>
</div> <div class="panel-heading-btn">
<div class="card-body"> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="info-group"> <div class="info-group">
@ -271,11 +277,15 @@
<!-- Statistics Sidebar --> <!-- Statistics Sidebar -->
<div class="col-lg-4 col-sm-12"> <div class="col-lg-4 col-sm-12">
<!-- Quick Stats --> <!-- Quick Stats -->
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-3">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Location Statistics</h5> <h4 class="panel-title">Location Statistics</h4>
</div> <div class="panel-heading-btn">
<div class="card-body"> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="stats-list"> <div class="stats-list">
<div class="stats-item"> <div class="stats-item">
<div class="stats-icon bg-primary"> <div class="stats-icon bg-primary">
@ -318,11 +328,15 @@
</div> </div>
<!-- Recent Activity --> <!-- Recent Activity -->
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-4">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Recent Activity</h5> <h4 class="panel-title">Recent Activity</h4>
</div> <div class="panel-heading-btn">
<div class="card-body"> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="activity-list"> <div class="activity-list">
<div class="activity-item"> <div class="activity-item">
<div class="activity-icon bg-success"> <div class="activity-icon bg-success">
@ -369,11 +383,15 @@
</div> </div>
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-5">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Quick Actions</h5> <h4 class="panel-title">Quick Actions</h4>
</div> <div class="panel-heading-btn">
<div class="card-body"> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="quick-actions"> <div class="quick-actions">
<a href="{% url 'inventory:stock_list' %}?location={{ location.id }}" class="btn btn-outline-primary w-100 mb-2"> <a href="{% url 'inventory:stock_list' %}?location={{ location.id }}" class="btn btn-outline-primary w-100 mb-2">
<i class="fa fa-list"></i> View Stock Items <i class="fa fa-list"></i> View Stock Items
@ -399,16 +417,18 @@
<!-- Stock Items in Location --> <!-- Stock Items in Location -->
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-6">
<div class="card-header"> <div class="panel-heading">
<h5 class="card-title">Stock Items in Location</h5> <h4 class="panel-title">Stock Items in Location</h4>
<div class="card-tools"> <div class="panel-heading-btn">
<button type="button" class="btn btn-sm btn-primary" onclick="refreshStockList()"> <button type="button" class="btn btn-xs btn-primary me-2" onclick="refreshStockList()">
<i class="fa fa-sync"></i> Refresh <i class="fa fa-sync"></i> Refresh
</button> </button>
</div> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
</div> <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<div class="card-body"> </div>
</div>
<div class="panel-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table" id="stockTable"> <table class="table" id="stockTable">
<thead> <thead>

View File

@ -2,7 +2,285 @@
{% load static %} {% load static %}
{% block title %}Inventory Locations - Inventory Management{% endblock %} {% block title %}Inventory Locations - Inventory Management{% endblock %}
{% block css %}
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.page-header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--card-color);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
color: white;
font-size: 1.25rem;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #495057;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.filters-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
align-items: end;
}
.orders-table-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
display: flex;
justify-content: between;
align-items: center;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-pending { background: #fff3cd; color: #856404; }
.status-approved { background: #d1ecf1; color: #0c5460; }
.status-ordered { background: #d4edda; color: #155724; }
.status-received { background: #d4edda; color: #155724; }
.status-cancelled { background: #f8d7da; color: #721c24; }
.priority-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.priority-low { background: #d4edda; color: #155724; }
.priority-medium { background: #fff3cd; color: #856404; }
.priority-high { background: #f8d7da; color: #721c24; }
.priority-urgent { background: #f5c6cb; color: #721c24; }
.action-buttons {
display: flex;
gap: 0.25rem;
}
.btn-action {
padding: 0.375rem 0.5rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.btn-view { background: #e3f2fd; color: #1976d2; }
.btn-edit { background: #fff3e0; color: #f57c00; }
.btn-delete { background: #ffebee; color: #d32f2f; }
.btn-approve { background: #e8f5e8; color: #2e7d32; }
.btn-action:hover {
transform: scale(1.05);
opacity: 0.8;
}
.bulk-actions {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
display: none;
}
.bulk-actions.show {
display: block;
}
.quick-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.quick-filter {
padding: 0.5rem 1rem;
border: 1px solid #dee2e6;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
text-decoration: none;
color: #495057;
}
.quick-filter:hover, .quick-filter.active {
background: #007bff;
color: white;
border-color: #007bff;
text-decoration: none;
}
.order-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.order-amount {
font-weight: bold;
color: #28a745;
}
.order-items {
font-size: 0.875rem;
color: #6c757d;
}
.supplier-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.supplier-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #007bff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
.table-actions {
display: flex;
justify-content: between;
align-items: center;
padding: 1rem 1.5rem;
background: #f8f9fa;
border-top: 1px solid #dee2e6;
}
@media (max-width: 768px) {
.page-header-section {
padding: 1.5rem;
}
.stats-cards {
grid-template-columns: repeat(2, 1fr);
}
.filter-row {
grid-template-columns: 1fr;
}
.quick-filters {
justify-content: center;
}
.action-buttons {
flex-direction: column;
}
.table-actions {
flex-direction: column;
gap: 1rem;
}
}
@media print {
.filters-section, .bulk-actions, .action-buttons, .table-actions {
display: none !important;
}
.section-header {
background: none;
border-bottom: 2px solid #000;
color: #000;
}
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="content"> <div class="content">
<div class="container-fluid"> <div class="container-fluid">
@ -24,57 +302,73 @@
</div> </div>
<!-- Location Statistics --> <!-- Location Statistics -->
<div class="row"> <div class="row mb-4" >
<div class="col-lg-3 col-sm-6 col-12"> <!-- Room Status Stats -->
<div class="dash-widget"> <div class="col-xl-3 col-md-6 mb-3">
<div class="dash-widgetimg"> <div class="card bg-gradient-success text-white">
<span><i class="fas fa-map-marker-alt"></i></span> <div class="card-body">
</div> <div class="d-flex justify-content-between align-items-center">
<div class="dash-widgetcontent"> <div>
<h5>{{ stats.total_locations|default:24 }}</h5> <div class="h4 mb-1">{{ stats.total_locations|default:7 }}</div>
<h6>Total Locations</h6> <div class="small">Total Locations</div>
</div> </div>
</div> <div class="fa-2x">
</div> <i class="fas fa-map-marker-alt"></i>
<div class="col-lg-3 col-sm-6 col-12"> </div>
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-check-circle"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ stats.active_locations|default:22 }}</h5>
<h6>Active Locations</h6>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-lock"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ stats.controlled_locations|default:6 }}</h5>
<h6>Controlled Access</h6>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-thermometer-half"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ stats.climate_controlled|default:8 }}</h5>
<h6>Climate Controlled</h6>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="h4 mb-1">{{ stats.active_locations|default:22 }}</div>
<div class="small">Active Locations</div>
</div>
<div class="fa-2x">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-danger text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="h4 mb-1">{{ stats.controlled_locations|default:6 }}</div>
<div class="small">Controlled Access</div>
</div>
<div class="fa-2x">
<i class="fas fa-lock"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="h4 mb-1">{{ stats.climate_controlled|default:8 }}</div>
<div class="small">Climate Controlled</div>
</div>
<div class="fa-2x">
<i class="fas fa-thermometer-half"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters and Search --> <!-- Filters and Search -->
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="table-top"> <div class="d-flex justify-content-between align-items-center">
<div class="search-set"> <div class="search-set">
<div class="search-path"> <div class="search-path">
<a class="btn btn-filter" id="filter_search"> <a class="btn btn-filter" id="filter_search">
@ -82,31 +376,27 @@
<span><i class="fas fa-times"></i></span> <span><i class="fas fa-times"></i></span>
</a> </a>
</div> </div>
<div class="search-input"> <div class="input-group">
<a class="btn btn-searchset"> <a class="btn btn-sm btn-outline-secondary" id="search_input">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</a> </a>
<input type="text" id="searchInput" placeholder="Search locations..."> <input class="form-control form-control-sm" type="text" id="searchInput" placeholder="Search locations...">
</div> </div>
</div> </div>
<div class="wordset"> <div class="d-flex justify-content-between align-items-center">
<ul>
<li> <a class="btn brn-xs btn-outline-primary me-1" data-bs-toggle="tooltip" data-bs-placement="top" title="PDF" onclick="exportToPDF()">
<a data-bs-toggle="tooltip" data-bs-placement="top" title="PDF" onclick="exportToPDF()">
<i class="fas fa-file-pdf"></i> <i class="fas fa-file-pdf"></i>
</a> </a>
</li>
<li> <a class="btn brn-xs btn-outline-success me-1" data-bs-toggle="tooltip" data-bs-placement="top" title="Excel" onclick="exportToExcel()">
<a data-bs-toggle="tooltip" data-bs-placement="top" title="Excel" onclick="exportToExcel()">
<i class="fas fa-file-excel"></i> <i class="fas fa-file-excel"></i>
</a> </a>
</li>
<li> <a class="btn brn-xs btn-outline-red" data-bs-toggle="tooltip" data-bs-placement="top" title="Print" onclick="printTable()">
<a data-bs-toggle="tooltip" data-bs-placement="top" title="Print" onclick="printTable()">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</a> </a>
</li>
</ul>
</div> </div>
</div> </div>

View File

@ -286,7 +286,7 @@
<!-- Stock Alerts Modal --> <!-- Stock Alerts Modal -->
<div class="modal fade" id="stock-alerts-modal" tabindex="-1"> <div class="modal fade" id="stock-alerts-modal" tabindex="-1">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Stock Alerts</h5> <h5 class="modal-title">Stock Alerts</h5>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}Stock Alerts - {{ block.super }}{% endblock %}
{% block css %} {% block css %}
<style> <style>

BIN
templates/quality/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -22,7 +22,7 @@
<a href="{% url 'quality:incident_report_create' %}" class="btn btn-danger"> <a href="{% url 'quality:incident_report_create' %}" class="btn btn-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Report Incident <i class="fas fa-exclamation-triangle me-2"></i>Report Incident
</a> </a>
<a href="{% url 'quality:quality_measurement_create' %}" class="btn btn-primary"> <a href="{% url 'quality:measurement_create' %}" class="btn btn-primary">
<i class="fas fa-chart-line me-2"></i>Record Measurement <i class="fas fa-chart-line me-2"></i>Record Measurement
</a> </a>
<a href="{% url 'quality:risk_assessment_create' %}" class="btn btn-warning"> <a href="{% url 'quality:risk_assessment_create' %}" class="btn btn-warning">
@ -367,7 +367,7 @@
<button type="button" class="btn btn-outline-secondary" onclick="refreshMeasurements()"> <button type="button" class="btn btn-outline-secondary" onclick="refreshMeasurements()">
<i class="fas fa-sync-alt"></i> <i class="fas fa-sync-alt"></i>
</button> </button>
<a href="{% url 'quality:quality_measurement_list' %}" class="btn btn-outline-primary"> <a href="{% url 'quality:measurement_list' %}" class="btn btn-outline-primary">
<i class="fas fa-list me-1"></i>View All <i class="fas fa-list me-1"></i>View All
</a> </a>
</div> </div>
@ -416,7 +416,7 @@
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i> <i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Recent Measurements</h5> <h5 class="text-muted">No Recent Measurements</h5>
<p class="text-muted mb-3">No measurements have been recorded recently.</p> <p class="text-muted mb-3">No measurements have been recorded recently.</p>
<a href="{% url 'quality:quality_measurement_create' %}" class="btn btn-primary"> <a href="{% url 'quality:measurement_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Record Measurement <i class="fas fa-plus me-2"></i>Record Measurement
</a> </a>
</div> </div>
@ -516,7 +516,7 @@
</a> </a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a href="{% url 'quality:quality_measurement_create' %}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3"> <a href="{% url 'quality:measurement_create' %}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3">
<i class="fas fa-chart-line fa-2x mb-2"></i> <i class="fas fa-chart-line fa-2x mb-2"></i>
<span>Record Measurement</span> <span>Record Measurement</span>
</a> </a>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -548,24 +548,24 @@ function saveDraft(isAutoSave = false) {
}); });
} }
function searchIncidents() { {#function searchIncidents() {#}
const query = $('#incidentSearch').val(); {# const query = $('#incidentSearch').val();#}
if (query.length < 2) { {# if (query.length < 2) {#}
toastr.warning('Please enter at least 2 characters to search'); {# toastr.warning('Please enter at least 2 characters to search');#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "quality:search_incidents" %}', {# url: '{% url "quality:search_incidents" %}',#}
data: { q: query }, {# data: { q: query },#}
success: function(response) { {# success: function(response) {#}
displayIncidentSearchResults(response.incidents); {# displayIncidentSearchResults(response.incidents);#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to search incidents'); {# toastr.error('Failed to search incidents');#}
} {# }#}
}); {# });#}
} {# }#}
function displayIncidentSearchResults(incidents) { function displayIncidentSearchResults(incidents) {
const resultsDiv = $('#incidentSearchResults'); const resultsDiv = $('#incidentSearchResults');
@ -639,35 +639,35 @@ function removeIncident(incidentId) {
toastr.info('Incident removed from report'); toastr.info('Incident removed from report');
} }
function uploadFile(file) { {#function uploadFile(file) {#}
if (file.size > 10 * 1024 * 1024) { // 10MB limit {# if (file.size > 10 * 1024 * 1024) { // 10MB limit#}
toastr.error('File size exceeds 10MB limit'); {# toastr.error('File size exceeds 10MB limit');#}
return; {# return;#}
} {# }#}
{# #}
const formData = new FormData(); {# const formData = new FormData();#}
formData.append('file', file); {# formData.append('file', file);#}
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}'); {# formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "quality:upload_attachment" %}', {# url: '{% url "quality:upload_attachment" %}',#}
method: 'POST', {# method: 'POST',#}
data: formData, {# data: formData,#}
processData: false, {# processData: false,#}
contentType: false, {# contentType: false,#}
success: function(response) { {# success: function(response) {#}
if (response.success) { {# if (response.success) {#}
addAttachmentToList(response.attachment); {# addAttachmentToList(response.attachment);#}
toastr.success('File uploaded successfully'); {# toastr.success('File uploaded successfully');#}
} else { {# } else {#}
toastr.error(response.message || 'Failed to upload file'); {# toastr.error(response.message || 'Failed to upload file');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to upload file'); {# toastr.error('Failed to upload file');#}
} {# }#}
}); {# });#}
} {# }#}
function addAttachmentToList(attachment) { function addAttachmentToList(attachment) {
const attachmentHtml = ` const attachmentHtml = `
@ -690,29 +690,29 @@ function addAttachmentToList(attachment) {
$('#attachmentList').append(attachmentHtml); $('#attachmentList').append(attachmentHtml);
} }
function removeAttachment(attachmentId) { {#function removeAttachment(attachmentId) {#}
if (confirm('Remove this attachment?')) { {# if (confirm('Remove this attachment?')) {#}
$.ajax({ {# $.ajax({#}
url: '{% url "quality:remove_attachment" %}', {# url: '{% url "quality:remove_attachment" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'attachment_id': attachmentId, {# 'attachment_id': attachmentId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.success) { {# if (response.success) {#}
$(`[data-attachment-id="${attachmentId}"]`).remove(); {# $(`[data-attachment-id="${attachmentId}"]`).remove();#}
toastr.success('Attachment removed'); {# toastr.success('Attachment removed');#}
} else { {# } else {#}
toastr.error('Failed to remove attachment'); {# toastr.error('Failed to remove attachment');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to remove attachment'); {# toastr.error('Failed to remove attachment');#}
} {# }#}
}); {# });#}
} {# }#}
} {# }#}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -665,106 +665,106 @@ function filterByPerformance(performance) {
filterIndicators(); filterIndicators();
} }
function calculateIndicator(indicatorId) { {#function calculateIndicator(indicatorId) {#}
$.ajax({ {# $.ajax({#}
url: '{% url "quality:calculate_indicator" %}', {# url: '{% url "quality:calculate_indicator" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'indicator_id': indicatorId, {# 'indicator_id': indicatorId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.success) { {# if (response.success) {#}
toastr.success('Indicator calculated successfully'); {# toastr.success('Indicator calculated successfully');#}
// Update the row with new data {# // Update the row with new data#}
updateIndicatorRow(indicatorId, response.indicator); {# updateIndicatorRow(indicatorId, response.indicator);#}
} else { {# } else {#}
toastr.error(response.message || 'Failed to calculate indicator'); {# toastr.error(response.message || 'Failed to calculate indicator');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to calculate indicator'); {# toastr.error('Failed to calculate indicator');#}
} {# }#}
}); {# });#}
} {# }#}
function bulkCalculate() { {#function bulkCalculate() {#}
if (confirm('Calculate all active indicators? This may take a few moments.')) { {# if (confirm('Calculate all active indicators? This may take a few moments.')) {#}
$.ajax({ {# $.ajax({#}
url: '{% url "quality:bulk_calculate_indicators" %}', {# url: '{% url "quality:bulk_calculate_indicators" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
toastr.success(response.message || 'Bulk calculation completed'); {# toastr.success(response.message || 'Bulk calculation completed');#}
location.reload(); {# location.reload();#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to calculate indicators'); {# toastr.error('Failed to calculate indicators');#}
} {# }#}
}); {# });#}
} {# }#}
} {# }#}
function bulkCalculateSelected() { {#function bulkCalculateSelected() {#}
const selectedIds = $('.indicator-checkbox:checked').map(function() { {# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
return $(this).val(); {# return $(this).val();#}
}).get(); {# }).get();#}
{# #}
{# if (selectedIds.length === 0) {#}
{# toastr.warning('Please select indicators to calculate');#}
{# return;#}
{# }#}
{# #}
{# $.ajax({#}
{# url: '{% url "quality:bulk_calculate_indicators" %}',#}
{# method: 'POST',#}
{# data: {#}
{# 'indicator_ids': selectedIds,#}
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
{# },#}
{# success: function(response) {#}
{# toastr.success(response.message || 'Selected indicators calculated');#}
{# location.reload();#}
{# },#}
{# error: function() {#}
{# toastr.error('Failed to calculate selected indicators');#}
{# }#}
{# });#}
{# }#}
if (selectedIds.length === 0) { {#function bulkUpdateStatus() {#}
toastr.warning('Please select indicators to calculate'); {# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
return; {# return $(this).val();#}
} {# }).get();#}
{# #}
$.ajax({ {# if (selectedIds.length === 0) {#}
url: '{% url "quality:bulk_calculate_indicators" %}', {# toastr.warning('Please select indicators to update');#}
method: 'POST', {# return;#}
data: { {# }#}
'indicator_ids': selectedIds, {# #}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# // Show status selection modal#}
}, {# const newStatus = prompt('Enter new status (active, inactive, draft, archived):');#}
success: function(response) { {# if (newStatus && ['active', 'inactive', 'draft', 'archived'].includes(newStatus)) {#}
toastr.success(response.message || 'Selected indicators calculated'); {# $.ajax({#}
location.reload(); {# url: '{% url "quality:bulk_update_indicator_status" %}',#}
}, {# method: 'POST',#}
error: function() { {# data: {#}
toastr.error('Failed to calculate selected indicators'); {# 'indicator_ids': selectedIds,#}
} {# 'status': newStatus,#}
}); {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
} {# },#}
{# success: function(response) {#}
function bulkUpdateStatus() { {# toastr.success(response.message || 'Status updated successfully');#}
const selectedIds = $('.indicator-checkbox:checked').map(function() { {# location.reload();#}
return $(this).val(); {# },#}
}).get(); {# error: function() {#}
{# toastr.error('Failed to update status');#}
if (selectedIds.length === 0) { {# }#}
toastr.warning('Please select indicators to update'); {# });#}
return; {# }#}
} {# }#}
// Show status selection modal
const newStatus = prompt('Enter new status (active, inactive, draft, archived):');
if (newStatus && ['active', 'inactive', 'draft', 'archived'].includes(newStatus)) {
$.ajax({
url: '{% url "quality:bulk_update_indicator_status" %}',
method: 'POST',
data: {
'indicator_ids': selectedIds,
'status': newStatus,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
toastr.success(response.message || 'Status updated successfully');
location.reload();
},
error: function() {
toastr.error('Failed to update status');
}
});
}
}
function bulkExport() { function bulkExport() {
const selectedIds = $('.indicator-checkbox:checked').map(function() { const selectedIds = $('.indicator-checkbox:checked').map(function() {
@ -782,58 +782,58 @@ function bulkExport() {
} }
} }
function bulkArchive() { {#function bulkArchive() {#}
const selectedIds = $('.indicator-checkbox:checked').map(function() { {# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
return $(this).val(); {# return $(this).val();#}
}).get(); {# }).get();#}
{# #}
{# if (selectedIds.length === 0) {#}
{# toastr.warning('Please select indicators to archive');#}
{# return;#}
{# }#}
{# #}
{# if (confirm(`Archive ${selectedIds.length} selected indicator(s)?`)) {#}
{# $.ajax({#}
{# url: '{% url "quality:bulk_archive_indicators" %}',#}
{# method: 'POST',#}
{# data: {#}
{# 'indicator_ids': selectedIds,#}
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
{# },#}
{# success: function(response) {#}
{# toastr.success(response.message || 'Indicators archived successfully');#}
{# location.reload();#}
{# },#}
{# error: function() {#}
{# toastr.error('Failed to archive indicators');#}
{# }#}
{# });#}
{# }#}
{# }#}
if (selectedIds.length === 0) { {#function exportData(format, selectedIds = null) {#}
toastr.warning('Please select indicators to archive'); {# let url = '{% url "quality:export_indicators" %}?format=' + format;#}
return; {# #}
} {# if (selectedIds) {#}
{# url += '&ids=' + selectedIds.join(',');#}
if (confirm(`Archive ${selectedIds.length} selected indicator(s)?`)) { {# }#}
$.ajax({ {# #}
url: '{% url "quality:bulk_archive_indicators" %}', {# // Add current filters to export#}
method: 'POST', {# const search = $('#searchInput').val();#}
data: { {# const status = $('#statusFilter').val();#}
'indicator_ids': selectedIds, {# const category = $('#categoryFilter').val();#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# const performance = $('#performanceFilter').val();#}
}, {# const department = $('#departmentFilter').val();#}
success: function(response) { {# #}
toastr.success(response.message || 'Indicators archived successfully'); {# if (search) url += '&search=' + encodeURIComponent(search);#}
location.reload(); {# if (status) url += '&status=' + status;#}
}, {# if (category) url += '&category=' + category;#}
error: function() { {# if (performance) url += '&performance=' + performance;#}
toastr.error('Failed to archive indicators'); {# if (department) url += '&department=' + department;#}
} {# #}
}); {# window.open(url, '_blank');#}
} {# toastr.info('Export started');#}
} {# }#}
function exportData(format, selectedIds = null) {
let url = '{% url "quality:export_indicators" %}?format=' + format;
if (selectedIds) {
url += '&ids=' + selectedIds.join(',');
}
// Add current filters to export
const search = $('#searchInput').val();
const status = $('#statusFilter').val();
const category = $('#categoryFilter').val();
const performance = $('#performanceFilter').val();
const department = $('#departmentFilter').val();
if (search) url += '&search=' + encodeURIComponent(search);
if (status) url += '&status=' + status;
if (category) url += '&category=' + category;
if (performance) url += '&performance=' + performance;
if (department) url += '&department=' + department;
window.open(url, '_blank');
toastr.info('Export started');
}
function createIndicator() { function createIndicator() {
const formData = { const formData = {
@ -847,7 +847,7 @@ function createIndicator() {
}; };
$.ajax({ $.ajax({
url: '{% url "quality:create_indicator" %}', url: '{% url "quality:quality_indicator_create" %}',
method: 'POST', method: 'POST',
data: formData, data: formData,
success: function(response) { success: function(response) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,964 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Delete Project - {{ project.name }}{% endblock %}
{% block extra_css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.delete-layout {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.delete-main {
display: flex;
flex-direction: column;
gap: 2rem;
}
.delete-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-content {
padding: 1.5rem;
}
.warning-box {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-left: 4px solid #ffc107;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.warning-icon {
color: #856404;
font-size: 1.25rem;
margin-right: 0.5rem;
}
.warning-title {
font-weight: 600;
color: #856404;
margin-bottom: 0.5rem;
}
.warning-text {
color: #856404;
margin: 0;
}
.danger-box {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-left: 4px solid #dc3545;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.danger-icon {
color: #721c24;
font-size: 1.25rem;
margin-right: 0.5rem;
}
.danger-title {
font-weight: 600;
color: #721c24;
margin-bottom: 0.5rem;
}
.danger-text {
color: #721c24;
margin: 0;
}
.project-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
}
.info-value {
font-size: 0.875rem;
color: #495057;
font-weight: 500;
}
.impact-list {
list-style: none;
padding: 0;
margin: 0;
}
.impact-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.impact-item:hover {
background: #f8f9fa;
}
.impact-item:last-child {
border-bottom: none;
}
.impact-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
flex-shrink: 0;
}
.impact-content {
flex: 1;
}
.impact-title {
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
.impact-description {
font-size: 0.875rem;
color: #6c757d;
}
.impact-count {
font-size: 0.875rem;
font-weight: 600;
color: #dc3545;
background: #fff5f5;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.related-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
margin-bottom: 0.75rem;
background: white;
transition: all 0.2s;
}
.related-item:hover {
border-color: #dc3545;
background: #fff5f5;
}
.related-icon {
width: 32px;
height: 32px;
border-radius: 0.375rem;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
flex-shrink: 0;
}
.related-info {
flex: 1;
}
.related-title {
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
.related-meta {
font-size: 0.75rem;
color: #6c757d;
}
.related-status {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-active { background: #e8f5e8; color: #2e7d32; }
.status-completed { background: #e3f2fd; color: #1976d2; }
.status-pending { background: #fff3e0; color: #f57c00; }
.confirmation-section {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.confirmation-title {
font-size: 1.25rem;
font-weight: 600;
color: #495057;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.confirmation-steps {
list-style: none;
padding: 0;
margin: 0;
}
.confirmation-step {
display: flex;
align-items: start;
gap: 0.75rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: white;
border-radius: 0.375rem;
border: 1px solid #dee2e6;
}
.step-number {
width: 24px;
height: 24px;
border-radius: 50%;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
.step-description {
font-size: 0.875rem;
color: #6c757d;
}
.confirmation-checkbox {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: #fff5f5;
border: 2px solid #dc3545;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
}
.confirmation-checkbox input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: #dc3545;
}
.confirmation-text {
font-weight: 600;
color: #721c24;
}
.delete-actions {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
display: flex;
justify-content: between;
align-items: center;
gap: 1rem;
}
.btn-group {
display: flex;
gap: 0.5rem;
}
.alternative-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alternative-text {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 0.375rem;
}
.stat-number {
font-size: 1.5rem;
font-weight: bold;
color: #dc3545;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
}
@media (max-width: 1200px) {
.delete-layout {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.delete-sidebar {
order: -1;
}
}
@media (max-width: 768px) {
.delete-header {
padding: 1.5rem;
}
.project-info {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.delete-actions {
flex-direction: column;
align-items: stretch;
}
.btn-group {
justify-content: center;
}
}
@media print {
.delete-sidebar, .delete-actions {
display: none !important;
}
.delete-layout {
grid-template-columns: 1fr;
}
.section-header {
background: none;
border-bottom: 2px solid #000;
color: #000;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'quality:dashboard' %}">Quality</a></li>
<li class="breadcrumb-item"><a href="{% url 'quality:project_list' %}">Projects</a></li>
<li class="breadcrumb-item"><a href="{% url 'quality:project_detail' project.id %}">{{ project.name|truncatechars:20 }}</a></li>
<li class="breadcrumb-item active">Delete</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-trash me-2"></i>Delete Project
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'quality:project_detail' project.id %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Project
</a>
</div>
</div>
<!-- Delete Header -->
<div class="delete-header">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="mb-2">
<i class="fas fa-exclamation-triangle me-2"></i>
Delete Project: {{ project.name }}
</h2>
<p class="mb-0">This action will permanently delete the project and all associated data. Please review the impact before proceeding.</p>
</div>
<div class="col-md-4 text-md-end">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">{{ project.team_members.count|default:0 }}</div>
<div class="stat-label">Team Members</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ project.milestones.count|default:0 }}</div>
<div class="stat-label">Milestones</div>
</div>
</div>
</div>
</div>
</div>
<div class="delete-layout">
<!-- Main Content -->
<div class="delete-main">
<!-- Warning Messages -->
<div class="danger-box">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle danger-icon"></i>
<div>
<div class="danger-title">Permanent Deletion Warning</div>
<div class="danger-text">
This action cannot be undone. All project data, including milestones, deliverables,
team assignments, and progress tracking will be permanently deleted.
</div>
</div>
</div>
</div>
{% if project.status == 'active' %}
<div class="warning-box">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-circle warning-icon"></i>
<div>
<div class="warning-title">Active Project Warning</div>
<div class="warning-text">
This project is currently active with ongoing work. Consider archiving instead of deleting
to preserve historical data and team contributions.
</div>
</div>
</div>
</div>
{% endif %}
<!-- Project Information -->
<div class="section-card">
<div class="section-header">
<i class="fas fa-info-circle"></i>
Project Information
</div>
<div class="section-content">
<div class="project-info">
<div class="info-item">
<div class="info-label">Project Name</div>
<div class="info-value">{{ project.name }}</div>
</div>
<div class="info-item">
<div class="info-label">Project ID</div>
<div class="info-value">{{ project.project_id|default:project.id }}</div>
</div>
<div class="info-item">
<div class="info-label">Department</div>
<div class="info-value">{{ project.department.name|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Status</div>
<div class="info-value">{{ project.get_status_display }}</div>
</div>
<div class="info-item">
<div class="info-label">Priority</div>
<div class="info-value">{{ project.get_priority_display }}</div>
</div>
<div class="info-item">
<div class="info-label">Created</div>
<div class="info-value">{{ project.created_at|date:"M d, Y" }}</div>
</div>
<div class="info-item">
<div class="info-label">Progress</div>
<div class="info-value">{{ project.progress|default:0 }}%</div>
</div>
<div class="info-item">
<div class="info-label">Manager</div>
<div class="info-value">{{ project.manager.get_full_name|default:"Not assigned" }}</div>
</div>
</div>
{% if project.description %}
<div class="mt-3">
<div class="info-label">Description</div>
<div class="info-value">{{ project.description|truncatechars:200 }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Impact Analysis -->
<div class="section-card">
<div class="section-header">
<i class="fas fa-chart-line"></i>
Deletion Impact Analysis
</div>
<div class="section-content">
<ul class="impact-list">
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-users"></i>
</div>
<div class="impact-content">
<div class="impact-title">Team Members</div>
<div class="impact-description">
{{ project.team_members.count }} team member{{ project.team_members.count|pluralize }} will lose access to this project
</div>
</div>
<div class="impact-count">{{ project.team_members.count }}</div>
</li>
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-flag-checkered"></i>
</div>
<div class="impact-content">
<div class="impact-title">Milestones</div>
<div class="impact-description">
All project milestones and their progress tracking will be deleted
</div>
</div>
<div class="impact-count">{{ project.milestones.count|default:0 }}</div>
</li>
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-box"></i>
</div>
<div class="impact-content">
<div class="impact-title">Deliverables</div>
<div class="impact-description">
All project deliverables and associated files will be permanently removed
</div>
</div>
<div class="impact-count">{{ project.deliverables.count|default:0 }}</div>
</li>
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-comments"></i>
</div>
<div class="impact-content">
<div class="impact-title">Comments & Notes</div>
<div class="impact-description">
All project discussions and documentation will be lost
</div>
</div>
<div class="impact-count">{{ project.comments.count|default:0 }}</div>
</li>
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-history"></i>
</div>
<div class="impact-content">
<div class="impact-title">Activity History</div>
<div class="impact-description">
Complete audit trail and activity history will be permanently deleted
</div>
</div>
<div class="impact-count">{{ project.activities.count|default:0 }}</div>
</li>
<li class="impact-item">
<div class="impact-icon">
<i class="fas fa-chart-bar"></i>
</div>
<div class="impact-content">
<div class="impact-title">Reports & Analytics</div>
<div class="impact-description">
Historical performance data and reports will no longer be available
</div>
</div>
<div class="impact-count"></div>
</li>
</ul>
</div>
</div>
<!-- Confirmation Steps -->
<div class="confirmation-section">
<div class="confirmation-title">
<i class="fas fa-clipboard-check"></i>
Deletion Confirmation Process
</div>
<ol class="confirmation-steps">
<li class="confirmation-step">
<div class="step-number">1</div>
<div class="step-content">
<div class="step-title">Review Impact</div>
<div class="step-description">
Carefully review all data that will be permanently deleted above
</div>
</div>
</li>
<li class="confirmation-step">
<div class="step-number">2</div>
<div class="step-content">
<div class="step-title">Notify Team</div>
<div class="step-description">
Ensure all team members are aware of the project deletion
</div>
</div>
</li>
<li class="confirmation-step">
<div class="step-number">3</div>
<div class="step-content">
<div class="step-title">Backup Data</div>
<div class="step-description">
Export any important data or reports before deletion
</div>
</div>
</li>
<li class="confirmation-step">
<div class="step-number">4</div>
<div class="step-content">
<div class="step-title">Confirm Deletion</div>
<div class="step-description">
Check the confirmation box and proceed with deletion
</div>
</div>
</li>
</ol>
<form method="post" id="delete-form">
{% csrf_token %}
<div class="confirmation-checkbox">
<input type="checkbox" id="confirm-delete" name="confirm_delete" required>
<label for="confirm-delete" class="confirmation-text">
I understand that this action cannot be undone and will permanently delete
"{{ project.name }}" and all associated data.
</label>
</div>
<div class="form-group">
<label class="form-label">Type project name to confirm:</label>
<input type="text" class="form-control" id="project-name-confirm"
placeholder="Enter: {{ project.name }}" required>
<div class="form-text">Type the exact project name to enable deletion</div>
</div>
</form>
</div>
</div>
<!-- Sidebar -->
<div class="delete-sidebar">
<!-- Related Items -->
{% if project.related_findings.exists or project.related_audits.exists %}
<div class="section-card">
<div class="section-header">
<i class="fas fa-link"></i>
Related Items
</div>
<div class="section-content">
{% for finding in project.related_findings.all|slice:":5" %}
<div class="related-item">
<div class="related-icon">
<i class="fas fa-search"></i>
</div>
<div class="related-info">
<div class="related-title">{{ finding.title|truncatechars:30 }}</div>
<div class="related-meta">Finding • {{ finding.date_identified|date:"M d, Y" }}</div>
</div>
<span class="related-status status-{{ finding.status }}">
{{ finding.get_status_display }}
</span>
</div>
{% endfor %}
{% for audit in project.related_audits.all|slice:":5" %}
<div class="related-item">
<div class="related-icon">
<i class="fas fa-clipboard-check"></i>
</div>
<div class="related-info">
<div class="related-title">{{ audit.title|truncatechars:30 }}</div>
<div class="related-meta">Audit • {{ audit.date_scheduled|date:"M d, Y" }}</div>
</div>
<span class="related-status status-{{ audit.status }}">
{{ audit.get_status_display }}
</span>
</div>
{% endfor %}
{% if project.related_findings.count > 5 or project.related_audits.count > 5 %}
<div class="text-center text-muted mt-2">
+{{ project.related_findings.count|add:project.related_audits.count|add:"-5" }} more related items
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Team Members -->
{% if project.team_members.exists %}
<div class="section-card">
<div class="section-header">
<i class="fas fa-users"></i>
Affected Team Members
</div>
<div class="section-content">
{% for member in project.team_members.all %}
<div class="related-item">
<div class="related-icon" style="background: #007bff;">
{{ member.first_name.0|upper }}{{ member.last_name.0|upper }}
</div>
<div class="related-info">
<div class="related-title">{{ member.get_full_name }}</div>
<div class="related-meta">{{ member.email }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Alternative Actions -->
<div class="section-card">
<div class="section-header">
<i class="fas fa-lightbulb"></i>
Alternative Actions
</div>
<div class="section-content">
<div class="alternative-text">
Consider these alternatives to permanent deletion:
</div>
<div class="alternative-actions">
<a href="{% url 'quality:project_archive' project.id %}" class="btn btn-outline-info btn-sm">
<i class="fas fa-archive me-1"></i>Archive Project
</a>
<button type="button" class="btn btn-outline-warning btn-sm" onclick="pauseProject()">
<i class="fas fa-pause me-1"></i>Pause Project
</button>
<button type="button" class="btn btn-outline-success btn-sm" onclick="exportProject()">
<i class="fas fa-download me-1"></i>Export Data
</button>
<a href="{% url 'quality:project_edit' project.id %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-edit me-1"></i>Edit Instead
</a>
</div>
</div>
</div>
<!-- Deletion History -->
<div class="section-card">
<div class="section-header">
<i class="fas fa-history"></i>
Recent Deletions
</div>
<div class="section-content">
{% for deleted_project in recent_deletions %}
<div class="related-item">
<div class="related-icon" style="background: #6c757d;">
<i class="fas fa-trash"></i>
</div>
<div class="related-info">
<div class="related-title">{{ deleted_project.name|truncatechars:25 }}</div>
<div class="related-meta">Deleted {{ deleted_project.deleted_at|timesince }} ago</div>
</div>
</div>
{% empty %}
<div class="text-center text-muted py-3">
<i class="fas fa-history fa-2x mb-2"></i>
<p>No recent deletions</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Delete Actions -->
<div class="delete-actions">
<div class="alternative-text">
<strong>Need help?</strong> Contact your system administrator if you're unsure about deleting this project.
</div>
<div class="btn-group">
<a href="{% url 'quality:project_detail' project.id %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Cancel
</a>
<button type="button" class="btn btn-outline-info" onclick="exportProject()">
<i class="fas fa-download me-1"></i>Export First
</button>
<button type="submit" form="delete-form" class="btn btn-danger" id="delete-btn" disabled>
<i class="fas fa-trash me-1"></i>Delete Project
</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Enable delete button only when conditions are met
function checkDeleteConditions() {
const confirmChecked = $('#confirm-delete').is(':checked');
const nameMatch = $('#project-name-confirm').val() === '{{ project.name }}';
$('#delete-btn').prop('disabled', !(confirmChecked && nameMatch));
}
$('#confirm-delete, #project-name-confirm').on('change keyup', checkDeleteConditions);
// Form submission confirmation
$('#delete-form').on('submit', function(e) {
if (!confirm('Are you absolutely sure you want to delete this project? This action cannot be undone.')) {
e.preventDefault();
}
});
});
function pauseProject() {
if (confirm('Pause this project instead of deleting it?')) {
fetch(`/quality/projects/{{ project.id }}/pause/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('Project paused successfully', 'success');
setTimeout(() => {
window.location.href = '{% url "quality:project_detail" project.id %}';
}, 1500);
} else {
showAlert('Error pausing project', 'danger');
}
})
.catch(error => {
showAlert('Error pausing project', 'danger');
});
}
}
function exportProject() {
showAlert('Preparing project export...', 'info');
window.open(`/quality/projects/{{ project.id }}/export/`, '_blank');
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -399,7 +399,7 @@ function insertVariable(variable) {
const before = text.substring(0, start); const before = text.substring(0, start);
const after = text.substring(end, text.length); const after = text.substring(end, text.length);
activeElement.value = before + `{{${variable}}}` + after; activeElement.value = before + `${variable}` + after;
activeElement.focus(); activeElement.focus();
activeElement.setSelectionRange(start + variable.length + 4, start + variable.length + 4); activeElement.setSelectionRange(start + variable.length + 4, start + variable.length + 4);
} else { } else {
@ -448,7 +448,7 @@ function replaceVariables(text, data) {
let result = text; let result = text;
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
const regex = new RegExp(`{{${key}}}`, 'g'); const regex = new RegExp(`${key}`, 'g');
result = result.replace(regex, data[key] || `[${key}]`); result = result.replace(regex, data[key] || `[${key}]`);
}); });