This commit is contained in:
Utkarsh Khedkar
2025-12-19 11:00:34 +05:30
parent 5dc9fc7db4
commit fc9a401a8c
11 changed files with 6113 additions and 730 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,896 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('page-title', 'Staff Management Dashboard')
@section('content')
<style>
.top-bar { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; }
.card { background:#fff; border:1px solid #e4e4e4; border-radius:6px; padding:1rem; box-shadow:0 1px 3px rgba(0,0,0,.03); }
table { width:100%; border-collapse:collapse; }
th, td { padding:.6rem .75rem; border-bottom:1px solid #f1f1f1; text-align:left; }
.btn { padding:.45rem .75rem; border-radius:4px; border:1px solid #ccc; background:#f7f7f7; cursor:pointer; }
.btn.primary { background:#0b74de; color:#fff; border-color:#0b74de; }
.actions a { margin-right:.5rem; color:#0b74de; text-decoration:none; }
.muted { color:#666; font-size:.95rem; }
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #f72585;
--success: #4cc9f0;
--warning: #f8961e;
--danger: #e63946;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--border: #e2e8f0;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--hover-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Search Bar - Similar to Shipment */
.search-staff-bar {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--gradient-primary);
border-radius: 16px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
margin-bottom: 30px;
color: white;
position: relative;
overflow: hidden;
}
.search-staff-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.1);
z-index: 0;
}
.search-staff-bar > * {
position: relative;
z-index: 1;
}
.search-staff-bar input,
.search-staff-bar select {
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
flex: 1;
min-width: 150px;
background: rgba(255,255,255,0.9);
font-weight: 500;
transition: all 0.3s ease;
color: var(--dark);
}
.search-staff-bar input:focus,
.search-staff-bar select:focus {
background: white;
box-shadow: 0 0 0 3px rgba(255,255,255,0.3);
outline: none;
}
.btn-add-staff {
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
white-space: nowrap;
font-weight: 600;
text-decoration: none;
}
.btn-add-staff:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.search-icon {
font-size: 20px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.user-icon {
font-size: 18px;
}
@media (max-width: 768px) {
.search-staff-bar {
flex-direction: column;
align-items: stretch;
}
.search-staff-bar input,
.search-staff-bar select {
width: 100%;
}
}
/* Card Styles - Same as Shipment */
.card {
border: none;
border-radius: 16px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--hover-shadow);
}
.card-header {
background: var(--gradient-primary);
color: white;
border: none;
padding: 20px 25px;
border-radius: 16px 16px 0 0 !important;
}
.card-header h5 {
margin: 0;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
/* Table Styles - Similar to Shipment */
.table-responsive {
border-radius: 0 0 16px 16px;
overflow-x: auto;
}
.table {
margin: 0;
border-collapse: separate;
border-spacing: 0;
width: 100%;
padding: 0;
}
.table thead th {
background: #f8f9fa;
border: none;
padding: 16px 12px;
font-weight: 700;
color: var(--dark);
text-align: left;
vertical-align: middle;
border-bottom: 2px solid var(--border);
position: relative;
}
.table tbody tr {
transition: all 0.3s ease;
}
.table tbody tr:hover {
background-color: #f8f9ff;
transform: scale(1.01);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 12px;
text-align: left;
vertical-align: middle;
border-bottom: 1px solid var(--border);
font-weight: 500;
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* Status Badges - Similar Style */
.badge {
padding: 6px 12px !important;
border-radius: 20px !important;
font-weight: 600 !important;
font-size: 12px !important;
border: 2px solid transparent !important;
min-width: 80px !important;
text-align: center !important;
display: inline-block !important;
line-height: 1.2 !important;
}
.badge-active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
color: #065f46 !important;
border-color: #10b981 !important;
}
.badge-inactive {
background: linear-gradient(135deg, #fecaca, #fca5a5) !important;
color: #991b1b !important;
border-color: #ef4444 !important;
}
.badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a) !important;
color: #92400e !important;
border-color: #f59e0b !important;
}
/* Employee ID Badge - Similar to Shipment ID */
.employee-id-badge {
font-family: 'Courier New', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
display: inline-block;
}
/* Action Buttons - Similar Style */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
padding: 6px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
border: none;
cursor: pointer;
}
.btn-edit {
background: linear-gradient(135deg, #4cc9f0, #4361ee);
color: white;
}
.btn-edit:hover {
background: linear-gradient(135deg, #38bdf8, #3a56d4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 201, 240, 0.3);
}
.btn-delete {
background: linear-gradient(135deg, #f87171, #ef4444);
color: white;
}
.btn-delete:hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Success Message - Similar Style */
.alert-success {
background: linear-gradient(135deg, #e6ffed, #d1f7e5);
border: 1px solid #b6f0c6;
border-left: 4px solid var(--success);
color: #0f5132;
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-success:before {
content: '✓';
background: var(--success);
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--gray);
}
.empty-state:before {
content: '👤';
font-size: 3rem;
display: block;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Role Badges */
.role-badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
}
/* Stats Cards - Similar to Shipment Totals */
.stats-cards {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--hover-shadow);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-icon.total {
background: linear-gradient(135deg, #e6f3ff, #c2d9ff);
color: var(--primary);
}
.stat-icon.active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #10b981;
}
.stat-content h3 {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: var(--dark);
}
.stat-content p {
color: var(--gray);
margin: 4px 0 0 0;
font-size: 0.875rem;
}
/* Pagination - Same as Shipment */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 25px;
border-top: 1px solid #eef3fb;
}
.pagination-info {
font-size: 13px;
color: #9ba5bb;
font-weight: 600;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
}
.pagination-btn:hover:not(:disabled) {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-btn:disabled {
background: #f8fafc;
color: #cbd5e0;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.6;
}
.pagination-page-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 36px;
text-align: center;
}
.pagination-page-btn:hover {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-page-btn.active {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-pages {
display: flex;
gap: 4px;
align-items: center;
}
.pagination-ellipsis {
color: #9ba5bb;
font-size: 13px;
padding: 0 4px;
}
@media (max-width: 768px) {
.stats-cards {
flex-direction: column;
}
.stat-card {
min-width: 100%;
}
.pagination-container {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.pagination-controls {
justify-content: center;
}
}
</style>
<div class="top-bar">
<h2>Staff</h2>
<a href="{{ route('admin.staff.create') }}" class="btn primary">Add Staff</a>
<div class="container-fluid py-4">
@if(session('success'))
<div class="alert-success">
{{ session('success') }}
</div>
@endif
{{-- Stats Cards --}}
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon total">
👥
</div>
<div class="stat-content">
<h3>{{ $staff->count() }}</h3>
<p>Total Staff</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon active">
</div>
<div class="stat-content">
<h3>{{ $staff->where('status', 'active')->count() }}</h3>
<p>Active Staff</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff); color: #8b5cf6;">
👑
</div>
<div class="stat-content">
<h3>{{ $staff->unique('role')->count() }}</h3>
<p>Unique Roles</p>
</div>
</div>
</div>
{{-- Search Bar --}}
<div class="search-staff-bar">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="Search by name, email, or employee ID...">
<div class="status-filter-container">
<select id="statusFilter" class="status-filter-select">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</div>
<select id="roleFilter">
<option value="all">All Roles</option>
@foreach($staff->unique('role')->pluck('role') as $role)
@if($role)
<option value="{{ $role }}">{{ ucfirst($role) }}</option>
@endif
@endforeach
</select>
<a href="{{ route('admin.staff.create') }}" class="btn-add-staff">
<span class="user-icon"></span>
Add Staff
</a>
</div>
{{-- Staff Table --}}
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people me-2"></i> Staff Management</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Employee ID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="staffTableBody">
@php
$totalStaff = count($staff);
@endphp
@forelse($staff as $s)
<tr class="staff-row" data-status="{{ $s->status }}" data-role="{{ $s->role ?? '' }}">
<td class="fw-bold">{{ $totalStaff - $loop->index }}</td>
<td>
<span class="employee-id-badge">{{ $s->employee_id }}</span>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
{{ strtoupper(substr($s->name, 0, 1)) }}
</div>
<span class="fw-medium">{{ $s->name }}</span>
</div>
</td>
<td>{{ $s->email }}</td>
<td>{{ $s->phone ?? '-' }}</td>
<td>
@if($s->role)
<span class="role-badge">{{ $s->role }}</span>
@else
<span class="text-muted">-</span>
@endif
</td>
<td>
<span class="badge badge-{{ $s->status }}">
{{ ucfirst($s->status) }}
</span>
</td>
<td>
<div class="action-buttons">
<a href="{{ route('admin.staff.edit', $s->id) }}" class="btn-action btn-edit">
<i class="bi bi-pencil"></i>
Edit
</a>
<form action="{{ route('admin.staff.destroy', $s->id) }}" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to delete this staff member?')">
@csrf
@method('DELETE')
<button type="submit" class="btn-action btn-delete">
<i class="bi bi-trash"></i>
Delete
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<div class="empty-state">
No staff members found
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Pagination --}}
<div class="pagination-container">
<div class="pagination-info" id="pageInfo">
Showing 1 to {{ $staff->count() }} of {{ $staff->count() }} entries
</div>
<div class="pagination-controls">
<button class="pagination-btn" id="prevPageBtn" title="Previous page" disabled>
<i class="bi bi-chevron-left"></i>
</button>
<div class="pagination-pages" id="paginationPages">
<button class="pagination-page-btn active">1</button>
</div>
<button class="pagination-btn" id="nextPageBtn" title="Next page" disabled>
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card">
@if(session('success'))
<div style="padding:.5rem; background:#e6ffed; border:1px solid #b6f0c6; margin-bottom:1rem;">{{ session('success') }}</div>
@endif
<script>
// Pagination state
let currentPage = 1;
const itemsPerPage = 10;
let allStaff = @json($staff);
let filteredStaff = [...allStaff];
<table>
<thead>
<tr>
<th>#</th>
<th>Employee ID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($staff as $s)
<tr>
<td>{{ $s->id }}</td>
<td class="muted">{{ $s->employee_id }}</td>
<td>{{ $s->name }}</td>
<td>{{ $s->email }}</td>
<td>{{ $s->phone }}</td>
<td>{{ $s->role ?? '-' }}</td>
<td>{{ ucfirst($s->status) }}</td>
<td class="actions">
<a href="{{ route('admin.staff.edit', $s->id) }}">Edit</a>
<form action="{{ route('admin.staff.destroy', $s->id) }}" method="POST" style="display:inline" onsubmit="return confirm('Delete this staff?')">
@csrf
@method('DELETE')
<button class="btn" type="submit">Delete</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="8" class="muted">No staff found.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
renderTable();
updatePaginationControls();
// Bind pagination events
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
// Filter functionality
const statusFilter = document.getElementById('statusFilter');
const searchInput = document.getElementById('searchInput');
const roleFilter = document.getElementById('roleFilter');
function filterStaff() {
const selectedStatus = statusFilter.value;
const searchTerm = searchInput.value.toLowerCase();
const selectedRole = roleFilter.value;
filteredStaff = allStaff.filter(staff => {
let include = true;
// Status filter
if (selectedStatus !== 'all' && staff.status !== selectedStatus) {
include = false;
}
// Role filter
if (selectedRole !== 'all') {
const staffRole = staff.role || '';
if (staffRole.toLowerCase() !== selectedRole.toLowerCase()) {
include = false;
}
}
// Search filter
if (searchTerm) {
const matchesSearch =
staff.name.toLowerCase().includes(searchTerm) ||
staff.email.toLowerCase().includes(searchTerm) ||
(staff.employee_id && staff.employee_id.toLowerCase().includes(searchTerm)) ||
(staff.phone && staff.phone.toLowerCase().includes(searchTerm));
if (!matchesSearch) include = false;
}
return include;
});
currentPage = 1;
renderTable();
updatePaginationControls();
}
// Event listeners for filters
statusFilter.addEventListener('change', filterStaff);
searchInput.addEventListener('input', filterStaff);
roleFilter.addEventListener('change', filterStaff);
// Initialize filter
filterStaff();
});
// Pagination Functions
function goToPreviousPage() {
if (currentPage > 1) {
currentPage--;
renderTable();
updatePaginationControls();
}
}
function goToNextPage() {
const totalPages = Math.ceil(filteredStaff.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTable();
updatePaginationControls();
}
}
function updatePaginationControls() {
const totalPages = Math.ceil(filteredStaff.length / itemsPerPage);
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
const pageInfo = document.getElementById('pageInfo');
const paginationPages = document.getElementById('paginationPages');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
// Update page info text
const startIndex = (currentPage - 1) * itemsPerPage + 1;
const endIndex = Math.min(currentPage * itemsPerPage, filteredStaff.length);
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${filteredStaff.length} entries`;
// Generate page numbers
paginationPages.innerHTML = '';
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
addPageButton(i, paginationPages);
}
} else {
addPageButton(1, paginationPages);
if (currentPage > 3) {
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
}
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
addPageButton(i, paginationPages);
}
if (currentPage < totalPages - 2) {
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
}
addPageButton(totalPages, paginationPages);
}
}
function addPageButton(pageNumber, container) {
const button = document.createElement('button');
button.className = 'pagination-page-btn';
if (pageNumber === currentPage) {
button.classList.add('active');
}
button.textContent = pageNumber;
button.addEventListener('click', () => {
currentPage = pageNumber;
renderTable();
updatePaginationControls();
});
container.appendChild(button);
}
// Render Table
function renderTable() {
const tbody = document.getElementById('staffTableBody');
if (filteredStaff.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<div class="empty-state">
No staff members found matching your criteria
</div>
</td>
</tr>
`;
return;
}
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedItems = filteredStaff.slice(startIndex, endIndex);
const sortedItems = [...paginatedItems].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
tbody.innerHTML = '';
sortedItems.forEach((staff, index) => {
const displayIndex = filteredStaff.length - (startIndex + index);
const row = document.createElement('tr');
row.className = 'staff-row';
row.setAttribute('data-status', staff.status);
row.setAttribute('data-role', staff.role || '');
row.innerHTML = `
<td class="fw-bold">${displayIndex}</td>
<td>
<span class="employee-id-badge">${staff.employee_id}</span>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
${staff.name ? staff.name.charAt(0).toUpperCase() : '?'}
</div>
<span class="fw-medium">${staff.name}</span>
</div>
</td>
<td>${staff.email}</td>
<td>${staff.phone || '-'}</td>
<td>
${staff.role ? `<span class="role-badge">${staff.role}</span>` : '<span class="text-muted">-</span>'}
</td>
<td>
<span class="badge badge-${staff.status}">
${staff.status.charAt(0).toUpperCase() + staff.status.slice(1)}
</span>
</td>
<td>
<div class="action-buttons">
<a href="/admin/staff/${staff.id}/edit" class="btn-action btn-edit">
<i class="bi bi-pencil"></i>
Edit
</a>
<form action="/admin/staff/${staff.id}" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to delete this staff member?')">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn-action btn-delete">
<i class="bi bi-trash"></i>
Delete
</button>
</form>
</div>
</td>
`;
tbody.appendChild(row);
});
}
</script>
@endsection