update
This commit is contained in:
parent
1f0a6bff5f
commit
0a037d3d9d
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
@ -720,7 +720,7 @@ class InventoryStockUpdateView(LoginRequiredMixin, UpdateView):
|
||||
template_name = 'inventory/stock/stock_form.html'
|
||||
|
||||
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):
|
||||
kwargs = super().get_form_kwargs()
|
||||
|
||||
Binary file not shown.
@ -493,10 +493,10 @@ class LabOrderDetailView(LoginRequiredMixin, DetailView):
|
||||
lab_order = self.object
|
||||
|
||||
# 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
|
||||
context['results'] = lab_order.results.all().order_by('-result_datetime')
|
||||
context['results'] = lab_order.results.all().order_by('-created_at')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
19
quality/migrations/0002_qualityindicator_current_value.py
Normal file
19
quality/migrations/0002_qualityindicator_current_value.py
Normal 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,
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -43,6 +43,7 @@ class QualityIndicator(models.Model):
|
||||
type = models.CharField(max_length=20, choices=TYPE_CHOICES)
|
||||
measurement_unit = models.CharField(max_length=50)
|
||||
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_critical = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
calculation_method = models.TextField()
|
||||
|
||||
@ -43,10 +43,10 @@ urlpatterns = [
|
||||
# ============================================================================
|
||||
# QUALITY MEASUREMENT URLS (LIMITED CRUD - Operational Data)
|
||||
# ============================================================================
|
||||
path('measurements/', views.QualityMeasurementListView.as_view(), name='quality_measurement_list'),
|
||||
path('measurements/create/', views.QualityMeasurementCreateView.as_view(), name='quality_measurement_create'),
|
||||
path('measurements/<int:pk>/', views.QualityMeasurementDetailView.as_view(), name='quality_measurement_detail'),
|
||||
path('measurements/<int:pk>/update/', views.QualityMeasurementUpdateView.as_view(), name='quality_measurement_update'),
|
||||
path('measurements/', views.QualityMeasurementListView.as_view(), name='measurement_list'),
|
||||
path('measurements/create/', views.QualityMeasurementCreateView.as_view(), name='measurement_create'),
|
||||
path('measurements/<int:pk>/', views.QualityMeasurementDetailView.as_view(), name='measurement_detail'),
|
||||
path('measurements/<int:pk>/update/', views.QualityMeasurementUpdateView.as_view(), name='measurement_update'),
|
||||
# Note: No delete view for measurements - operational tracking data
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -105,13 +105,13 @@ class QualityDashboardView(LoginRequiredMixin, TemplateView):
|
||||
# Recent measurements
|
||||
context['recent_measurements'] = QualityMeasurement.objects.filter(
|
||||
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
|
||||
context['active_projects'] = ImprovementProject.objects.filter(
|
||||
tenant=tenant,
|
||||
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
|
||||
context['indicator_performance'] = QualityIndicator.objects.filter(
|
||||
@ -137,7 +137,7 @@ class QualityIndicatorListView(LoginRequiredMixin, ListView):
|
||||
List all quality indicators with filtering and search.
|
||||
"""
|
||||
model = QualityIndicator
|
||||
template_name = 'quality/quality_indicator_list.html'
|
||||
template_name = 'quality/indicators/quality_indicator_list.html'
|
||||
context_object_name = 'quality_indicators'
|
||||
paginate_by = 25
|
||||
|
||||
@ -175,13 +175,13 @@ class QualityIndicatorListView(LoginRequiredMixin, ListView):
|
||||
elif performance == 'below_target':
|
||||
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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'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', ''),
|
||||
})
|
||||
return context
|
||||
@ -192,7 +192,7 @@ class QualityIndicatorDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a quality indicator.
|
||||
"""
|
||||
model = QualityIndicator
|
||||
template_name = 'quality/quality_indicator_detail.html'
|
||||
template_name = 'quality/indicators/quality_indicator_detail.html'
|
||||
context_object_name = 'quality_indicator'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -252,7 +252,7 @@ class QualityIndicatorCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cr
|
||||
"""
|
||||
model = QualityIndicator
|
||||
form_class = QualityIndicatorForm
|
||||
template_name = 'quality/quality_indicator_form.html'
|
||||
template_name = 'quality/indicators/quality_indicator_form.html'
|
||||
permission_required = 'quality.add_qualityindicator'
|
||||
success_url = reverse_lazy('quality:quality_indicator_list')
|
||||
|
||||
@ -284,7 +284,7 @@ class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
|
||||
"""
|
||||
model = QualityIndicator
|
||||
form_class = QualityIndicatorForm
|
||||
template_name = 'quality/quality_indicator_form.html'
|
||||
template_name = 'quality/indicators/quality_indicator_form.html'
|
||||
permission_required = 'quality.change_qualityindicator'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -297,7 +297,7 @@ class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='QUALITY_INDICATOR_UPDATED',
|
||||
model='QualityIndicator',
|
||||
@ -317,7 +317,7 @@ class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
|
||||
Delete a quality indicator (soft delete by deactivating).
|
||||
"""
|
||||
model = QualityIndicator
|
||||
template_name = 'quality/quality_indicator_confirm_delete.html'
|
||||
template_name = 'quality/indicators/quality_indicator_confirm_delete.html'
|
||||
permission_required = 'quality.delete_qualityindicator'
|
||||
success_url = reverse_lazy('quality:quality_indicator_list')
|
||||
|
||||
@ -341,7 +341,7 @@ class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
|
||||
self.object.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='QUALITY_INDICATOR_DEACTIVATED',
|
||||
model='QualityIndicator',
|
||||
@ -362,7 +362,7 @@ class ImprovementProjectListView(LoginRequiredMixin, ListView):
|
||||
List all improvement projects with filtering and search.
|
||||
"""
|
||||
model = ImprovementProject
|
||||
template_name = 'quality/improvement_project_list.html'
|
||||
template_name = 'quality/projects/project_list.html'
|
||||
context_object_name = 'improvement_projects'
|
||||
paginate_by = 25
|
||||
|
||||
@ -393,7 +393,7 @@ class ImprovementProjectListView(LoginRequiredMixin, ListView):
|
||||
if 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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -410,7 +410,7 @@ class ImprovementProjectDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an improvement project.
|
||||
"""
|
||||
model = ImprovementProject
|
||||
template_name = 'quality/improvement_project_detail.html'
|
||||
template_name = 'quality/projects/project_detail.html'
|
||||
context_object_name = 'improvement_project'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -445,7 +445,7 @@ class ImprovementProjectCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = ImprovementProject
|
||||
form_class = ImprovementProjectForm
|
||||
template_name = 'quality/improvement_project_form.html'
|
||||
template_name = 'quality/projects/project_form.html'
|
||||
permission_required = 'quality.add_improvementproject'
|
||||
success_url = reverse_lazy('quality:improvement_project_list')
|
||||
|
||||
@ -476,7 +476,7 @@ class ImprovementProjectUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = ImprovementProject
|
||||
form_class = ImprovementProjectForm
|
||||
template_name = 'quality/improvement_project_form.html'
|
||||
template_name = 'quality/projects/project_form.html'
|
||||
permission_required = 'quality.change_improvementproject'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -509,7 +509,7 @@ class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
Delete an improvement project.
|
||||
"""
|
||||
model = ImprovementProject
|
||||
template_name = 'quality/improvement_project_confirm_delete.html'
|
||||
template_name = 'quality/projects/project_confirm_delete.html'
|
||||
permission_required = 'quality.delete_improvementproject'
|
||||
success_url = reverse_lazy('quality:improvement_project_list')
|
||||
|
||||
@ -521,7 +521,7 @@ class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
project_name = self.object.project_name
|
||||
|
||||
# Log the action before deletion
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='IMPROVEMENT_PROJECT_DELETED',
|
||||
model='ImprovementProject',
|
||||
@ -543,7 +543,7 @@ class AuditPlanListView(LoginRequiredMixin, ListView):
|
||||
List all audit plans with filtering and search.
|
||||
"""
|
||||
model = AuditPlan
|
||||
template_name = 'quality/audit_plan_list.html'
|
||||
template_name = 'quality/audits/audit_list.html'
|
||||
context_object_name = 'audit_plans'
|
||||
paginate_by = 25
|
||||
|
||||
@ -594,7 +594,7 @@ class AuditPlanDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an audit plan.
|
||||
"""
|
||||
model = AuditPlan
|
||||
template_name = 'quality/audit_plan_detail.html'
|
||||
template_name = 'quality/audits/audit_detail.html'
|
||||
context_object_name = 'audit_plan'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -628,7 +628,7 @@ class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVie
|
||||
"""
|
||||
model = AuditPlan
|
||||
form_class = AuditPlanForm
|
||||
template_name = 'quality/audit_plan_form.html'
|
||||
template_name = 'quality/audits/audit_form.html'
|
||||
permission_required = 'quality.add_auditplan'
|
||||
success_url = reverse_lazy('quality:audit_plan_list')
|
||||
|
||||
@ -637,7 +637,7 @@ class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVie
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='AUDIT_PLAN_CREATED',
|
||||
model='AuditPlan',
|
||||
@ -658,7 +658,7 @@ class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
|
||||
Update an audit plan (limited to status and notes after start).
|
||||
"""
|
||||
model = AuditPlan
|
||||
template_name = 'quality/audit_plan_update_form.html'
|
||||
template_name = 'quality/audits/audit_form.html'
|
||||
permission_required = 'quality.change_auditplan'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -682,7 +682,7 @@ class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='AUDIT_PLAN_UPDATED',
|
||||
model='AuditPlan',
|
||||
@ -706,7 +706,7 @@ class QualityMeasurementListView(LoginRequiredMixin, ListView):
|
||||
List all quality measurements with filtering and search.
|
||||
"""
|
||||
model = QualityMeasurement
|
||||
template_name = 'quality/quality_measurement_list.html'
|
||||
template_name = 'quality/measurements/measurement_list.html'
|
||||
context_object_name = 'quality_measurements'
|
||||
paginate_by = 25
|
||||
|
||||
@ -741,7 +741,7 @@ class QualityMeasurementListView(LoginRequiredMixin, ListView):
|
||||
'indicators': QualityIndicator.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
is_active=True
|
||||
).order_by('indicator_name'),
|
||||
).order_by('name'),
|
||||
'statuses': QualityMeasurement._meta.get_field('status').choices,
|
||||
})
|
||||
return context
|
||||
@ -752,7 +752,7 @@ class QualityMeasurementDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a quality measurement.
|
||||
"""
|
||||
model = QualityMeasurement
|
||||
template_name = 'quality/quality_measurement_detail.html'
|
||||
template_name = 'quality/measurements/measurement_detail.html'
|
||||
context_object_name = 'quality_measurement'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -765,7 +765,7 @@ class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = QualityMeasurement
|
||||
form_class = QualityMeasurementForm
|
||||
template_name = 'quality/quality_measurement_form.html'
|
||||
template_name = 'quality/measurements/measurement_form.html'
|
||||
permission_required = 'quality.add_qualitymeasurement'
|
||||
success_url = reverse_lazy('quality:quality_measurement_list')
|
||||
|
||||
@ -781,7 +781,7 @@ class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
indicator.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='QUALITY_MEASUREMENT_CREATED',
|
||||
model='QualityMeasurement',
|
||||
@ -803,7 +803,7 @@ class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = QualityMeasurement
|
||||
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'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -821,7 +821,7 @@ class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='QUALITY_MEASUREMENT_UPDATED',
|
||||
model='QualityMeasurement',
|
||||
@ -845,7 +845,7 @@ class RiskAssessmentListView(LoginRequiredMixin, ListView):
|
||||
List all risk assessments with filtering and search.
|
||||
"""
|
||||
model = RiskAssessment
|
||||
template_name = 'quality/risk_assessment_list.html'
|
||||
template_name = 'quality/risk_assessments/risk_assessment_list.html'
|
||||
context_object_name = 'risk_assessments'
|
||||
paginate_by = 25
|
||||
|
||||
@ -893,7 +893,7 @@ class RiskAssessmentDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a risk assessment.
|
||||
"""
|
||||
model = RiskAssessment
|
||||
template_name = 'quality/risk_assessment_detail.html'
|
||||
template_name = 'quality/risk_assessments/risk_assessment_detail.html'
|
||||
context_object_name = 'risk_assessment'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -906,7 +906,7 @@ class RiskAssessmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
"""
|
||||
model = RiskAssessment
|
||||
form_class = RiskAssessmentForm
|
||||
template_name = 'quality/risk_assessment_form.html'
|
||||
template_name = 'quality/risk_assessments/risk_assessment_form.html'
|
||||
permission_required = 'quality.add_riskassessment'
|
||||
success_url = reverse_lazy('quality:risk_assessment_list')
|
||||
|
||||
@ -937,7 +937,7 @@ class RiskAssessmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
|
||||
Update risk assessment (limited after approval).
|
||||
"""
|
||||
model = RiskAssessment
|
||||
template_name = 'quality/risk_assessment_update_form.html'
|
||||
template_name = 'quality/risk_assessments/risk_assessment_form.html'
|
||||
permission_required = 'quality.change_riskassessment'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -985,7 +985,7 @@ class IncidentReportListView(LoginRequiredMixin, ListView):
|
||||
List all incident reports with filtering and search.
|
||||
"""
|
||||
model = IncidentReport
|
||||
template_name = 'quality/incident_report_list.html'
|
||||
template_name = 'quality/incident_reports/incident_report_list.html'
|
||||
context_object_name = 'incident_reports'
|
||||
paginate_by = 25
|
||||
|
||||
@ -1042,7 +1042,7 @@ class IncidentReportDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an incident report.
|
||||
"""
|
||||
model = IncidentReport
|
||||
template_name = 'quality/incident_report_detail.html'
|
||||
template_name = 'quality/incident_reports/incident_report_detail.html'
|
||||
context_object_name = 'incident_report'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1055,7 +1055,7 @@ class IncidentReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
"""
|
||||
model = IncidentReport
|
||||
form_class = IncidentReportForm
|
||||
template_name = 'quality/incident_report_form.html'
|
||||
template_name = 'quality/incident_reports/incident_report_form.html'
|
||||
permission_required = 'quality.add_incidentreport'
|
||||
success_url = reverse_lazy('quality:incident_report_list')
|
||||
|
||||
@ -1093,7 +1093,7 @@ class AuditFindingListView(LoginRequiredMixin, ListView):
|
||||
List all audit findings with filtering and search.
|
||||
"""
|
||||
model = AuditFinding
|
||||
template_name = 'quality/audit_finding_list.html'
|
||||
template_name = 'quality/findings/finding_list.html'
|
||||
context_object_name = 'audit_findings'
|
||||
paginate_by = 25
|
||||
|
||||
@ -1144,7 +1144,7 @@ class AuditFindingDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an audit finding.
|
||||
"""
|
||||
model = AuditFinding
|
||||
template_name = 'quality/audit_finding_detail.html'
|
||||
template_name = 'quality/findings/finding_detail.html'
|
||||
context_object_name = 'audit_finding'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1157,7 +1157,7 @@ class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
|
||||
"""
|
||||
model = AuditFinding
|
||||
form_class = AuditFindingForm
|
||||
template_name = 'quality/audit_finding_form.html'
|
||||
template_name = 'quality/findings/finding_form.html'
|
||||
permission_required = 'quality.add_auditfinding'
|
||||
success_url = reverse_lazy('quality:audit_finding_list')
|
||||
|
||||
@ -1167,7 +1167,7 @@ class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='AUDIT_FINDING_CREATED',
|
||||
model='AuditFinding',
|
||||
@ -1279,7 +1279,7 @@ def approve_risk_assessment(request, assessment_id):
|
||||
assessment.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='RISK_ASSESSMENT_APPROVED',
|
||||
model='RiskAssessment',
|
||||
|
||||
Binary file not shown.
@ -139,7 +139,7 @@ class ReportTemplateListView(LoginRequiredMixin, ListView):
|
||||
if active_only:
|
||||
queryset = queryset.filter(is_active=True)
|
||||
|
||||
return queryset.order_by('template_name')
|
||||
return queryset.order_by('name')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -167,8 +167,8 @@ class ReportTemplateDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
# Get recent reports using this template
|
||||
context['recent_reports'] = RadiologyReport.objects.filter(
|
||||
template=template,
|
||||
tenant=self.request.user.tenant
|
||||
template_used=template,
|
||||
study__tenant=self.request.user.tenant
|
||||
).select_related('study__order__patient').order_by('-created_at')[:10]
|
||||
|
||||
return context
|
||||
@ -190,7 +190,7 @@ class ReportTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='REPORT_TEMPLATE_CREATED',
|
||||
model='ReportTemplate',
|
||||
@ -224,7 +224,7 @@ class ReportTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='REPORT_TEMPLATE_UPDATED',
|
||||
model='ReportTemplate',
|
||||
@ -687,12 +687,12 @@ class DICOMImageListView(LoginRequiredMixin, ListView):
|
||||
List all DICOM images with filtering and search.
|
||||
"""
|
||||
model = DICOMImage
|
||||
template_name = 'radiology/dicom_image_list.html'
|
||||
template_name = 'radiology/dicom/dicom_file_list.html'
|
||||
context_object_name = 'dicom_images'
|
||||
paginate_by = 50
|
||||
|
||||
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
|
||||
series_id = self.request.GET.get('series')
|
||||
@ -710,7 +710,7 @@ class DICOMImageListView(LoginRequiredMixin, ListView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'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],
|
||||
})
|
||||
return context
|
||||
|
||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@ -189,18 +189,18 @@
|
||||
<div class="row">
|
||||
<!-- Recent Purchase Orders -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-cart me-2"></i>Recent Purchase Orders
|
||||
</h5>
|
||||
<a href="{% url 'inventory:purchase_order_list' %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</a>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fas fa-shopping-cart me-2"></i>Recent Purchase Orders</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<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>
|
||||
<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 class="card-body p-0">
|
||||
<div class="panel-body">
|
||||
{% if recent_orders %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@ -228,8 +228,8 @@
|
||||
{{ order.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>${{ order.total_amount|floatformat:2 }}</td>
|
||||
<td>{{ order.order_date|date:"M d" }}</td>
|
||||
<td><span class="symbol">ê</span>{{ order.total_amount|floatformat:'2g' }}</td>
|
||||
<td>{{ order.order_date|date:" Y M d" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -251,18 +251,18 @@
|
||||
|
||||
<!-- Low Stock Alerts -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Low Stock Alerts
|
||||
</h5>
|
||||
<a href="{% url 'inventory:stock_list' %}?stock_status=low" class="btn btn-outline-warning btn-sm">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</a>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fas fa-exclamation-triangle me-2"></i>Low Stock Alerts</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<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>
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
{% if low_stock_alerts %}
|
||||
{% for alert in low_stock_alerts %}
|
||||
<div class="d-flex align-items-center mb-3 p-3 bg-light rounded">
|
||||
@ -294,18 +294,18 @@
|
||||
<div class="row">
|
||||
<!-- Recent Stock Movements -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Recent Stock Movements
|
||||
</h5>
|
||||
<a href="{% url 'inventory:stock_list' %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</a>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fas fa-exchange-alt me-2"></i>Recent Stock Movements</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'inventory:stock_list' %}" class="btn btn-outline-primary btn-xs"><i class="fas fa-list me-1"></i>View All</a>
|
||||
<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 class="card-body p-0">
|
||||
<div class="panel-body">
|
||||
{% if recent_stock_movements %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@ -351,13 +351,17 @@
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||
</h5>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fas fa-bolt me-2"></i>Quick Actions</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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="row g-3">
|
||||
<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">
|
||||
@ -404,33 +408,44 @@
|
||||
<!-- Inventory Charts -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<canvas id="categoryChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<canvas id="stockStatusChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Dashboard functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@ -467,9 +482,9 @@ function initializeCharts() {
|
||||
new Chart(categoryCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {{ category_labels|safe }},
|
||||
labels: ['blue', 'green', 'yellow', 'red', 'purple', 'orange', 'pink'],
|
||||
datasets: [{
|
||||
data: {{ category_data|safe }},
|
||||
data: [16, 12, 14, 10, 18, 20, 4],
|
||||
backgroundColor: [
|
||||
'#0d6efd', '#198754', '#ffc107', '#dc3545',
|
||||
'#6f42c1', '#fd7e14', '#20c997', '#6c757d'
|
||||
@ -497,7 +512,7 @@ function initializeCharts() {
|
||||
labels: ['In Stock', 'Low Stock', 'Out of Stock', 'Expired'],
|
||||
datasets: [{
|
||||
data: [
|
||||
{{ in_stock_count }},
|
||||
{{ total_items }},
|
||||
{{ low_stock_items }},
|
||||
{{ out_of_stock_count }},
|
||||
{{ expired_items }}
|
||||
|
||||
@ -34,18 +34,20 @@
|
||||
<!-- Location Overview -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-sm-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Location Information</h5>
|
||||
<div class="card-tools">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Location Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
{% if location.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
<span class="badge bg-success me-2">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
<span class="badge bg-danger me-2">Inactive</span>
|
||||
{% 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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-group">
|
||||
@ -127,11 +129,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Environmental Controls -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Environmental Controls & Capacity</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Environmental Controls & Capacity</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-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-group">
|
||||
@ -271,11 +277,15 @@
|
||||
<!-- Statistics Sidebar -->
|
||||
<div class="col-lg-4 col-sm-12">
|
||||
<!-- Quick Stats -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Location Statistics</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Location Statistics</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-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-item">
|
||||
<div class="stats-icon bg-primary">
|
||||
@ -318,11 +328,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Recent Activity</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Recent Activity</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-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-item">
|
||||
<div class="activity-icon bg-success">
|
||||
@ -369,11 +383,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Quick Actions</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-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="quick-actions">
|
||||
<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
|
||||
@ -399,16 +417,18 @@
|
||||
<!-- Stock Items in Location -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Stock Items in Location</h5>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="refreshStockList()">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Stock Items in Location</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<button type="button" class="btn btn-xs btn-primary me-2" onclick="refreshStockList()">
|
||||
<i class="fa fa-sync"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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="table-responsive">
|
||||
<table class="table" id="stockTable">
|
||||
<thead>
|
||||
|
||||
@ -2,7 +2,285 @@
|
||||
{% load static %}
|
||||
|
||||
{% 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 %}
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
@ -24,57 +302,73 @@
|
||||
</div>
|
||||
|
||||
<!-- Location Statistics -->
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="dash-widget">
|
||||
<div class="dash-widgetimg">
|
||||
<span><i class="fas fa-map-marker-alt"></i></span>
|
||||
</div>
|
||||
<div class="dash-widgetcontent">
|
||||
<h5>{{ stats.total_locations|default:24 }}</h5>
|
||||
<h6>Total 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-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 class="row mb-4" >
|
||||
<!-- Room Status Stats -->
|
||||
<div class="col-xl-3 col-md-6 mb-3">
|
||||
<div class="card bg-gradient-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="h4 mb-1">{{ stats.total_locations|default:7 }}</div>
|
||||
<div class="small">Total Locations</div>
|
||||
</div>
|
||||
<div class="fa-2x">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
</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 -->
|
||||
<div class="card">
|
||||
<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-path">
|
||||
<a class="btn btn-filter" id="filter_search">
|
||||
@ -82,31 +376,27 @@
|
||||
<span><i class="fas fa-times"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="search-input">
|
||||
<a class="btn btn-searchset">
|
||||
<div class="input-group">
|
||||
<a class="btn btn-sm btn-outline-secondary" id="search_input">
|
||||
<i class="fas fa-search"></i>
|
||||
</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 class="wordset">
|
||||
<ul>
|
||||
<li>
|
||||
<a data-bs-toggle="tooltip" data-bs-placement="top" title="PDF" onclick="exportToPDF()">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
||||
<a class="btn brn-xs btn-outline-primary me-1" data-bs-toggle="tooltip" data-bs-placement="top" title="PDF" onclick="exportToPDF()">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-bs-toggle="tooltip" data-bs-placement="top" title="Excel" onclick="exportToExcel()">
|
||||
|
||||
<a class="btn brn-xs btn-outline-success me-1" data-bs-toggle="tooltip" data-bs-placement="top" title="Excel" onclick="exportToExcel()">
|
||||
<i class="fas fa-file-excel"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-bs-toggle="tooltip" data-bs-placement="top" title="Print" onclick="printTable()">
|
||||
|
||||
<a class="btn brn-xs btn-outline-red" data-bs-toggle="tooltip" data-bs-placement="top" title="Print" onclick="printTable()">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -286,7 +286,7 @@
|
||||
|
||||
<!-- Stock Alerts Modal -->
|
||||
<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-header">
|
||||
<h5 class="modal-title">Stock Alerts</h5>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Stock Alerts - {{ block.super }}{% endblock %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
|
||||
BIN
templates/quality/.DS_Store
vendored
Normal file
BIN
templates/quality/.DS_Store
vendored
Normal file
Binary file not shown.
@ -22,7 +22,7 @@
|
||||
<a href="{% url 'quality:incident_report_create' %}" class="btn btn-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Report Incident
|
||||
</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
|
||||
</a>
|
||||
<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()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@ -416,7 +416,7 @@
|
||||
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No Recent Measurements</h5>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@ -516,7 +516,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
<span>Record Measurement</span>
|
||||
</a>
|
||||
|
||||
1340
templates/quality/findings/finding_detail.html
Normal file
1340
templates/quality/findings/finding_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
1388
templates/quality/findings/finding_form.html
Normal file
1388
templates/quality/findings/finding_form.html
Normal file
File diff suppressed because it is too large
Load Diff
1277
templates/quality/findings/finding_list.html
Normal file
1277
templates/quality/findings/finding_list.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -548,24 +548,24 @@ function saveDraft(isAutoSave = false) {
|
||||
});
|
||||
}
|
||||
|
||||
function searchIncidents() {
|
||||
const query = $('#incidentSearch').val();
|
||||
if (query.length < 2) {
|
||||
toastr.warning('Please enter at least 2 characters to search');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "quality:search_incidents" %}',
|
||||
data: { q: query },
|
||||
success: function(response) {
|
||||
displayIncidentSearchResults(response.incidents);
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to search incidents');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function searchIncidents() {#}
|
||||
{# const query = $('#incidentSearch').val();#}
|
||||
{# if (query.length < 2) {#}
|
||||
{# toastr.warning('Please enter at least 2 characters to search');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "quality:search_incidents" %}',#}
|
||||
{# data: { q: query },#}
|
||||
{# success: function(response) {#}
|
||||
{# displayIncidentSearchResults(response.incidents);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to search incidents');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function displayIncidentSearchResults(incidents) {
|
||||
const resultsDiv = $('#incidentSearchResults');
|
||||
@ -639,35 +639,35 @@ function removeIncident(incidentId) {
|
||||
toastr.info('Incident removed from report');
|
||||
}
|
||||
|
||||
function uploadFile(file) {
|
||||
if (file.size > 10 * 1024 * 1024) { // 10MB limit
|
||||
toastr.error('File size exceeds 10MB limit');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "quality:upload_attachment" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
addAttachmentToList(response.attachment);
|
||||
toastr.success('File uploaded successfully');
|
||||
} else {
|
||||
toastr.error(response.message || 'Failed to upload file');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to upload file');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function uploadFile(file) {#}
|
||||
{# if (file.size > 10 * 1024 * 1024) { // 10MB limit#}
|
||||
{# toastr.error('File size exceeds 10MB limit');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# const formData = new FormData();#}
|
||||
{# formData.append('file', file);#}
|
||||
{# formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "quality:upload_attachment" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: formData,#}
|
||||
{# processData: false,#}
|
||||
{# contentType: false,#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# addAttachmentToList(response.attachment);#}
|
||||
{# toastr.success('File uploaded successfully');#}
|
||||
{# } else {#}
|
||||
{# toastr.error(response.message || 'Failed to upload file');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to upload file');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function addAttachmentToList(attachment) {
|
||||
const attachmentHtml = `
|
||||
@ -690,29 +690,29 @@ function addAttachmentToList(attachment) {
|
||||
$('#attachmentList').append(attachmentHtml);
|
||||
}
|
||||
|
||||
function removeAttachment(attachmentId) {
|
||||
if (confirm('Remove this attachment?')) {
|
||||
$.ajax({
|
||||
url: '{% url "quality:remove_attachment" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'attachment_id': attachmentId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$(`[data-attachment-id="${attachmentId}"]`).remove();
|
||||
toastr.success('Attachment removed');
|
||||
} else {
|
||||
toastr.error('Failed to remove attachment');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to remove attachment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function removeAttachment(attachmentId) {#}
|
||||
{# if (confirm('Remove this attachment?')) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "quality:remove_attachment" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'attachment_id': attachmentId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# $(`[data-attachment-id="${attachmentId}"]`).remove();#}
|
||||
{# toastr.success('Attachment removed');#}
|
||||
{# } else {#}
|
||||
{# toastr.error('Failed to remove attachment');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to remove attachment');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -665,106 +665,106 @@ function filterByPerformance(performance) {
|
||||
filterIndicators();
|
||||
}
|
||||
|
||||
function calculateIndicator(indicatorId) {
|
||||
$.ajax({
|
||||
url: '{% url "quality:calculate_indicator" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'indicator_id': indicatorId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
toastr.success('Indicator calculated successfully');
|
||||
// Update the row with new data
|
||||
updateIndicatorRow(indicatorId, response.indicator);
|
||||
} else {
|
||||
toastr.error(response.message || 'Failed to calculate indicator');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to calculate indicator');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function calculateIndicator(indicatorId) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "quality:calculate_indicator" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'indicator_id': indicatorId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# toastr.success('Indicator calculated successfully');#}
|
||||
{# // Update the row with new data#}
|
||||
{# updateIndicatorRow(indicatorId, response.indicator);#}
|
||||
{# } else {#}
|
||||
{# toastr.error(response.message || 'Failed to calculate indicator');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to calculate indicator');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function bulkCalculate() {
|
||||
if (confirm('Calculate all active indicators? This may take a few moments.')) {
|
||||
$.ajax({
|
||||
url: '{% url "quality:bulk_calculate_indicators" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
toastr.success(response.message || 'Bulk calculation completed');
|
||||
location.reload();
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to calculate indicators');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function bulkCalculate() {#}
|
||||
{# if (confirm('Calculate all active indicators? This may take a few moments.')) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "quality:bulk_calculate_indicators" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# toastr.success(response.message || 'Bulk calculation completed');#}
|
||||
{# location.reload();#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to calculate indicators');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function bulkCalculateSelected() {
|
||||
const selectedIds = $('.indicator-checkbox:checked').map(function() {
|
||||
return $(this).val();
|
||||
}).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');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function bulkCalculateSelected() {#}
|
||||
{# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
|
||||
{# return $(this).val();#}
|
||||
{# }).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');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function bulkUpdateStatus() {
|
||||
const selectedIds = $('.indicator-checkbox:checked').map(function() {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
|
||||
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 bulkUpdateStatus() {#}
|
||||
{# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
|
||||
{# return $(this).val();#}
|
||||
{# }).get();#}
|
||||
{# #}
|
||||
{# 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() {
|
||||
const selectedIds = $('.indicator-checkbox:checked').map(function() {
|
||||
@ -782,58 +782,58 @@ function bulkExport() {
|
||||
}
|
||||
}
|
||||
|
||||
function bulkArchive() {
|
||||
const selectedIds = $('.indicator-checkbox:checked').map(function() {
|
||||
return $(this).val();
|
||||
}).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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function bulkArchive() {#}
|
||||
{# const selectedIds = $('.indicator-checkbox:checked').map(function() {#}
|
||||
{# return $(this).val();#}
|
||||
{# }).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');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
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 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() {
|
||||
const formData = {
|
||||
@ -847,7 +847,7 @@ function createIndicator() {
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "quality:create_indicator" %}',
|
||||
url: '{% url "quality:quality_indicator_create" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
|
||||
1068
templates/quality/measurements/measurement_confirm_delete.html
Normal file
1068
templates/quality/measurements/measurement_confirm_delete.html
Normal file
File diff suppressed because it is too large
Load Diff
1395
templates/quality/measurements/measurement_detail.html
Normal file
1395
templates/quality/measurements/measurement_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
1512
templates/quality/measurements/measurement_form.html
Normal file
1512
templates/quality/measurements/measurement_form.html
Normal file
File diff suppressed because it is too large
Load Diff
1283
templates/quality/measurements/measurement_list.html
Normal file
1283
templates/quality/measurements/measurement_list.html
Normal file
File diff suppressed because it is too large
Load Diff
964
templates/quality/projects/project_confirm_delete.html
Normal file
964
templates/quality/projects/project_confirm_delete.html
Normal 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 %}
|
||||
|
||||
1278
templates/quality/projects/project_detail.html
Normal file
1278
templates/quality/projects/project_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
1213
templates/quality/projects/project_form.html
Normal file
1213
templates/quality/projects/project_form.html
Normal file
File diff suppressed because it is too large
Load Diff
1159
templates/quality/projects/project_list.html
Normal file
1159
templates/quality/projects/project_list.html
Normal file
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
BIN
templates/radiology/.DS_Store
vendored
BIN
templates/radiology/.DS_Store
vendored
Binary file not shown.
1549
templates/radiology/dicom/dicom_analysis.html
Normal file
1549
templates/radiology/dicom/dicom_analysis.html
Normal file
File diff suppressed because it is too large
Load Diff
1144
templates/radiology/dicom/dicom_file_list.html
Normal file
1144
templates/radiology/dicom/dicom_file_list.html
Normal file
File diff suppressed because it is too large
Load Diff
1297
templates/radiology/dicom/dicom_metadata_editor.html
Normal file
1297
templates/radiology/dicom/dicom_metadata_editor.html
Normal file
File diff suppressed because it is too large
Load Diff
1713
templates/radiology/dicom/dicom_workflow.html
Normal file
1713
templates/radiology/dicom/dicom_workflow.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -399,7 +399,7 @@ function insertVariable(variable) {
|
||||
const before = text.substring(0, start);
|
||||
const after = text.substring(end, text.length);
|
||||
|
||||
activeElement.value = before + `{{${variable}}}` + after;
|
||||
activeElement.value = before + `${variable}` + after;
|
||||
activeElement.focus();
|
||||
activeElement.setSelectionRange(start + variable.length + 4, start + variable.length + 4);
|
||||
} else {
|
||||
@ -448,7 +448,7 @@ function replaceVariables(text, data) {
|
||||
|
||||
let result = text;
|
||||
Object.keys(data).forEach(key => {
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
const regex = new RegExp(`${key}`, 'g');
|
||||
result = result.replace(regex, data[key] || `[${key}]`);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user