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

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@ -720,7 +720,7 @@ class InventoryStockUpdateView(LoginRequiredMixin, UpdateView):
template_name = 'inventory/stock/stock_form.html'
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()

View File

@ -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

View File

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

View File

@ -43,6 +43,7 @@ class QualityIndicator(models.Model):
type = models.CharField(max_length=20, choices=TYPE_CHOICES)
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()

View File

@ -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
# ============================================================================

View File

@ -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',

View File

@ -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

Binary file not shown.

View File

@ -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">&#xea;</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 }}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

Binary file not shown.

View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -548,24 +548,24 @@ function saveDraft(isAutoSave = false) {
});
}
function searchIncidents() {
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 %}

View File

@ -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) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -399,7 +399,7 @@ function insertVariable(variable) {
const before = text.substring(0, start);
const 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}]`);
});