534 lines
27 KiB
HTML
534 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Suppliers - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="fas fa-truck me-2"></i>Suppliers
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'inventory:dashboard' %}">Inventory</a></li>
|
|
<li class="breadcrumb-item active">Suppliers</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="refreshSuppliers()">
|
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
|
</button>
|
|
<a href="{% url 'inventory:supplier_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add Supplier
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Total Suppliers</h6>
|
|
<h3 class="mb-0 text-primary">{{ total_suppliers }}</h3>
|
|
<small class="text-muted">{{ active_suppliers }} active</small>
|
|
</div>
|
|
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-truck fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Active Orders</h6>
|
|
<h3 class="mb-0 text-success">{{ active_orders }}</h3>
|
|
<small class="text-muted">In progress</small>
|
|
</div>
|
|
<div class="bg-success bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-shopping-cart fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">This Month</h6>
|
|
<h3 class="mb-0 text-info">${{ monthly_spending|floatformat:0 }}</h3>
|
|
<small class="text-muted">Total spending</small>
|
|
</div>
|
|
<div class="bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-dollar-sign fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Performance</h6>
|
|
<h3 class="mb-0 text-warning">{{ avg_rating|floatformat:1 }}/5</h3>
|
|
<small class="text-muted">Average rating</small>
|
|
</div>
|
|
<div class="bg-warning bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-star fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters and Search -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3" id="filterForm">
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">Search</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="search" name="search"
|
|
value="{{ request.GET.search }}" placeholder="Search suppliers...">
|
|
<button class="btn btn-outline-secondary" type="submit">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="status" class="form-label">Status</label>
|
|
<select class="form-select" id="status" name="status">
|
|
<option value="">All Status</option>
|
|
<option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>Active</option>
|
|
<option value="inactive" {% if request.GET.status == 'inactive' %}selected{% endif %}>Inactive</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="supplier_type" class="form-label">Type</label>
|
|
<select class="form-select" id="supplier_type" name="supplier_type">
|
|
<option value="">All Types</option>
|
|
<option value="MEDICAL_SUPPLIES" {% if request.GET.supplier_type == 'MEDICAL_SUPPLIES' %}selected{% endif %}>Medical Supplies</option>
|
|
<option value="PHARMACEUTICALS" {% if request.GET.supplier_type == 'PHARMACEUTICALS' %}selected{% endif %}>Pharmaceuticals</option>
|
|
<option value="EQUIPMENT" {% if request.GET.supplier_type == 'EQUIPMENT' %}selected{% endif %}>Equipment</option>
|
|
<option value="SERVICES" {% if request.GET.supplier_type == 'SERVICES' %}selected{% endif %}>Services</option>
|
|
<option value="OTHER" {% if request.GET.supplier_type == 'OTHER' %}selected{% endif %}>Other</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="rating" class="form-label">Min Rating</label>
|
|
<select class="form-select" id="rating" name="rating">
|
|
<option value="">Any Rating</option>
|
|
<option value="4" {% if request.GET.rating == '4' %}selected{% endif %}>4+ Stars</option>
|
|
<option value="3" {% if request.GET.rating == '3' %}selected{% endif %}>3+ Stars</option>
|
|
<option value="2" {% if request.GET.rating == '2' %}selected{% endif %}>2+ Stars</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label"> </label>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-filter me-1"></i>Filter
|
|
</button>
|
|
<a href="{% url 'inventory:supplier_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-times me-1"></i>Clear
|
|
</a>
|
|
<button type="button" class="btn btn-outline-info" onclick="exportSuppliers()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Suppliers List -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-list me-2"></i>Suppliers ({{ suppliers.count }})
|
|
</h5>
|
|
<div class="btn-group btn-group-sm">
|
|
<input type="checkbox" class="btn-check" id="selectAll" autocomplete="off">
|
|
<label class="btn btn-outline-secondary" for="selectAll">
|
|
<i class="fas fa-check-square me-1"></i>Select All
|
|
</label>
|
|
<button type="button" class="btn btn-outline-danger" id="bulkActions" disabled>
|
|
<i class="fas fa-trash me-1"></i>Bulk Actions
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if suppliers %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="40">
|
|
<input type="checkbox" class="form-check-input" id="selectAllTable">
|
|
</th>
|
|
<th>Supplier</th>
|
|
<th>Type</th>
|
|
<th>Contact</th>
|
|
<th>Rating</th>
|
|
<th>Status</th>
|
|
<th>Last Order</th>
|
|
<th width="120">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for supplier in suppliers %}
|
|
<tr>
|
|
<td>
|
|
<input type="checkbox" class="form-check-input supplier-checkbox"
|
|
value="{{ supplier.id }}">
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">
|
|
<i class="fas fa-truck text-white small"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ supplier.name }}</div>
|
|
<small class="text-muted">{{ supplier.supplier_code }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ supplier.get_supplier_type_display }}</span>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<div class="small">{{ supplier.contact_person|default:"—" }}</div>
|
|
<div class="text-muted small">
|
|
{% if supplier.phone %}
|
|
<i class="fas fa-phone me-1"></i>{{ supplier.phone }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-muted small">
|
|
{% if supplier.email %}
|
|
<i class="fas fa-envelope me-1"></i>{{ supplier.email|truncatechars:20 }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if supplier.rating %}
|
|
<div class="d-flex align-items-center">
|
|
<div class="text-warning me-1">
|
|
{% for i in "12345" %}
|
|
{% if forloop.counter <= supplier.rating %}
|
|
<i class="fas fa-star small"></i>
|
|
{% else %}
|
|
<i class="far fa-star small"></i>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
<small class="text-muted">({{ supplier.rating }})</small>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">Not rated</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if supplier.is_active %}success{% else %}secondary{% endif %}">
|
|
{% if supplier.is_active %}Active{% else %}Inactive{% endif %}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{% if supplier.last_order_date %}
|
|
<div>{{ supplier.last_order_date|date:"M d, Y" }}</div>
|
|
<small class="text-muted">{{ supplier.last_order_date|timesince }} ago</small>
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'inventory:supplier_detail' supplier.pk %}"
|
|
class="btn btn-outline-primary" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="{% url 'inventory:supplier_update' supplier.pk %}"
|
|
class="btn btn-outline-secondary" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
|
data-bs-toggle="dropdown">
|
|
<span class="visually-hidden">Toggle Dropdown</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="{% url 'inventory:purchase_order_create' %}?supplier={{ supplier.id }}">
|
|
<i class="fas fa-shopping-cart me-2"></i>Create Order
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="viewOrderHistory({{ supplier.id }})">
|
|
<i class="fas fa-history me-2"></i>Order History
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="{% url 'inventory:supplier_delete' supplier.pk %}">
|
|
<i class="fas fa-trash me-2"></i>Delete
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<div class="card-footer">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="text-muted">
|
|
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} suppliers
|
|
</div>
|
|
<nav aria-label="Suppliers pagination">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if page_obj.number == num %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ num }}</span>
|
|
</li>
|
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ num }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">{{ num }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-truck fa-3x text-muted mb-3"></i>
|
|
<h6 class="text-muted">No suppliers found</h6>
|
|
{% if request.GET.search or request.GET.status %}
|
|
<p class="text-muted">Try adjusting your search criteria</p>
|
|
<a href="{% url 'inventory:supplier_list' %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-times me-2"></i>Clear Filters
|
|
</a>
|
|
{% else %}
|
|
<p class="text-muted">Add your first supplier to get started</p>
|
|
<a href="{% url 'inventory:supplier_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add First Supplier
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Supplier list functionality
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-submit form on filter change
|
|
const filterSelects = document.querySelectorAll('#filterForm select');
|
|
filterSelects.forEach(select => {
|
|
select.addEventListener('change', function() {
|
|
document.getElementById('filterForm').submit();
|
|
});
|
|
});
|
|
|
|
// Bulk selection functionality
|
|
const selectAllTable = document.getElementById('selectAllTable');
|
|
const supplierCheckboxes = document.querySelectorAll('.supplier-checkbox');
|
|
const bulkActionsBtn = document.getElementById('bulkActions');
|
|
|
|
selectAllTable.addEventListener('change', function() {
|
|
supplierCheckboxes.forEach(checkbox => {
|
|
checkbox.checked = this.checked;
|
|
});
|
|
updateBulkActionsButton();
|
|
});
|
|
|
|
supplierCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', updateBulkActionsButton);
|
|
});
|
|
|
|
function updateBulkActionsButton() {
|
|
const checkedBoxes = document.querySelectorAll('.supplier-checkbox:checked');
|
|
bulkActionsBtn.disabled = checkedBoxes.length === 0;
|
|
|
|
if (checkedBoxes.length > 0) {
|
|
bulkActionsBtn.innerHTML = `<i class="fas fa-trash me-1"></i>Actions (${checkedBoxes.length})`;
|
|
} else {
|
|
bulkActionsBtn.innerHTML = '<i class="fas fa-trash me-1"></i>Bulk Actions';
|
|
}
|
|
}
|
|
|
|
// Bulk actions dropdown
|
|
bulkActionsBtn.addEventListener('click', function() {
|
|
const checkedBoxes = document.querySelectorAll('.supplier-checkbox:checked');
|
|
if (checkedBoxes.length > 0) {
|
|
const supplierIds = Array.from(checkedBoxes).map(cb => cb.value);
|
|
showBulkActionsModal(supplierIds);
|
|
}
|
|
});
|
|
});
|
|
|
|
function refreshSuppliers() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportSuppliers() {
|
|
const form = document.getElementById('filterForm');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams(formData);
|
|
params.append('export', 'csv');
|
|
|
|
window.location.href = '{% url "inventory:supplier_list" %}?' + params.toString();
|
|
}
|
|
|
|
function viewOrderHistory(supplierId) {
|
|
// Open order history modal or navigate to order history page
|
|
window.location.href = `/inventory/orders/?supplier=${supplierId}`;
|
|
}
|
|
|
|
function showBulkActionsModal(supplierIds) {
|
|
const modal = new bootstrap.Modal(document.getElementById('bulkActionsModal'));
|
|
document.getElementById('selectedSupplierIds').value = supplierIds.join(',');
|
|
modal.show();
|
|
}
|
|
|
|
// Real-time search
|
|
let searchTimeout;
|
|
document.getElementById('search').addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
document.getElementById('filterForm').submit();
|
|
}, 500);
|
|
});
|
|
</script>
|
|
|
|
<!-- Bulk Actions Modal -->
|
|
<div class="modal fade" id="bulkActionsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Bulk Actions</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="bulkActionsForm" method="post" action="">
|
|
{% csrf_token %}
|
|
<input type="hidden" id="selectedSupplierIds" name="supplier_ids">
|
|
|
|
<div class="mb-3">
|
|
<label for="bulkAction" class="form-label">Select Action</label>
|
|
<select class="form-select" id="bulkAction" name="action" required>
|
|
<option value="">Choose action...</option>
|
|
<option value="activate">Activate Suppliers</option>
|
|
<option value="deactivate">Deactivate Suppliers</option>
|
|
<option value="export">Export Selected</option>
|
|
<option value="delete" class="text-danger">Delete Suppliers</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
This action will be applied to all selected suppliers.
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="bulkActionsForm" class="btn btn-primary">Apply Action</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.bg-gradient {
|
|
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
|
}
|
|
|
|
.table th {
|
|
border-top: none;
|
|
font-weight: 600;
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.table td {
|
|
vertical-align: middle;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75em;
|
|
}
|
|
|
|
.btn-group-sm .btn {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.col-md-3, .col-md-2 {
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|