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

View File

@@ -224,5 +224,81 @@ class ShipmentController extends Controller
return view('admin.view_shipment', compact('shipment', 'dummyData'));
}
// App\Models\Shipment.php
public function orders()
{
return $this->belongsToMany(\App\Models\Order::class, 'shipment_items', 'shipment_id', 'order_id');
}
public function removeOrder(Shipment $shipment, Order $order)
{
// Remove row from pivot table shipment_items
ShipmentItem::where('shipment_id', $shipment->id)
->where('order_id', $order->id)
->delete(); // removes link shipment <-> order [web:41][web:45]
// Recalculate totals on this shipment (optional but recommended)
$orders = Order::whereIn(
'id',
ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id')
)->get();
$shipment->total_ctn = $orders->sum('ctn');
$shipment->total_qty = $orders->sum('qty');
$shipment->total_ttl_qty = $orders->sum('ttl_qty');
$shipment->total_cbm = $orders->sum('cbm');
$shipment->total_ttl_cbm = $orders->sum('ttl_cbm');
$shipment->total_kg = $orders->sum('kg');
$shipment->total_ttl_kg = $orders->sum('ttl_kg');
$shipment->total_amount = $orders->sum('ttl_amount');
$shipment->save();
// Redirect back to preview page where your blade is loaded
return redirect()
->route('admin.shipments.dummy', $shipment->id)
->with('success', 'Order removed from shipment successfully.');
}
public function addOrders(Request $request, Shipment $shipment)
{
$request->validate([
'order_ids' => 'required|array|min:1',
]);
// फक्त न वापरलेले orders घ्या
$orders = Order::whereIn('id', $request->order_ids)->get();
foreach ($orders as $order) {
// pivot मध्ये insert
ShipmentItem::create([
'shipment_id' => $shipment->id,
'order_id' => $order->id,
'order_ctn' => $order->ctn,
'order_qty' => $order->qty,
'order_ttl_qty' => $order->ttl_qty,
'order_ttl_amount' => $order->ttl_amount,
'order_ttl_kg' => $order->ttl_kg,
]);
}
// totals
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
$allOrders = Order::whereIn('id', $orderIds)->get();
$shipment->total_ctn = $allOrders->sum('ctn');
$shipment->total_qty = $allOrders->sum('qty');
$shipment->total_ttl_qty = $allOrders->sum('ttl_qty');
$shipment->total_cbm = $allOrders->sum('cbm');
$shipment->total_ttl_cbm = $allOrders->sum('ttl_cbm');
$shipment->total_kg = $allOrders->sum('kg');
$shipment->total_ttl_kg = $allOrders->sum('ttl_kg');
$shipment->total_amount = $allOrders->sum('ttl_amount');
$shipment->save();
return redirect()
->route('admin.shipments.dummy', $shipment->id)
->with('success', 'Orders added to shipment successfully.');
}
}

View File

@@ -1,96 +1,605 @@
@extends('admin.layouts.app')
@section('page-title', 'Chat Support')
@section('page-title', 'Chat Support Dashboard')
@section('content')
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
--card-shadow: 0 8px 25px rgba(0,0,0,0.08);
--card-shadow-hover: 0 15px 35px rgba(0,0,0,0.12);
--border-radius: 12px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
<div class="container py-4">
* {
box-sizing: border-box;
}
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
.chat-dashboard {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1.5rem;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
<div class="card shadow-sm">
<div class="card-body p-0">
.dashboard-header {
text-align: center;
margin-bottom: 2rem;
position: relative;
}
.dashboard-title {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
background: var(--primary-gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0 0 0.75rem 0;
position: relative;
display: inline-block;
}
.dashboard-title::before {
content: '💬';
position: absolute;
left: -2.5rem;
top: 50%;
transform: translateY(-50%);
font-size: 2rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
40% { transform: translateY(-50%) translateY(-8px); }
60% { transform: translateY(-50%) translateY(-4px); }
}
.dashboard-subtitle {
color: #64748b;
font-size: 1rem;
max-width: 500px;
margin: 0 auto;
line-height: 1.5;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
border-radius: var(--border-radius);
padding: 1.25rem 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: var(--card-shadow);
transition: var(--transition);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary-gradient);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--card-shadow-hover);
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-icon {
width: 55px;
height: 55px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.stat-total { background: var(--primary-gradient); color: white; }
.stat-open { background: var(--success-gradient); color: white; }
.stat-closed{ background: var(--danger-gradient); color: white; }
.stat-content h3 {
font-size: 2rem;
font-weight: 800;
margin: 0 0 0.125rem 0;
color: #1e293b;
}
.stat-content p {
margin: 0;
color: #64748b;
font-size: 0.9rem;
font-weight: 500;
}
.tickets-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--card-shadow);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.tickets-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #f1f5f9;
}
.tickets-title {
font-size: 1.4rem;
font-weight: 700;
color: #1e293b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tickets-count {
background: var(--primary-gradient);
color: white;
padding: 0.2rem 0.6rem;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 600;
}
/* ========= COMPACT TICKET CARD ========= */
.ticket-item {
background: white;
border-radius: 10px;
padding: 0.75rem 1rem;
margin-bottom: 0.6rem;
border: 1px solid #f1f5f9;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.ticket-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--primary-gradient);
transform: scaleY(0);
transition: var(--transition);
}
.ticket-item:hover {
transform: translateY(-2px);
box-shadow: var(--card-shadow-hover);
border-color: rgba(102, 126, 234, 0.15);
}
.ticket-item:hover::before {
transform: scaleY(1);
}
.ticket-header {
display: flex;
align-items: flex-start;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.ticket-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--info-gradient);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: white;
flex-shrink: 0;
box-shadow: 0 6px 16px rgba(86, 204, 242, 0.25);
position: relative;
overflow: hidden;
}
.ticket-avatar::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transition: left 0.5s;
}
.ticket-item:hover .ticket-avatar::after {
left: 100%;
}
.ticket-content {
flex: 1;
min-width: 0;
}
.ticket-name {
font-size: 0.95rem;
font-weight: 700;
color: #1e293b;
margin: 0 0 0.15rem 0;
display: flex;
align-items: center;
gap: 0.4rem;
}
.unread-count {
background: #ef4444;
color: white;
min-width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
box-shadow: 0 3px 10px rgba(239, 68, 68, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.ticket-preview {
color: #64748b;
font-size: 0.8rem;
line-height: 1.4;
margin-bottom: 0.25rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.ticket-time {
font-size: 0.75rem;
color: #94a3b8;
display: flex;
align-items: center;
gap: 0.25rem;
}
.ticket-time svg {
width: 12px;
height: 12px;
flex-shrink: 0;
}
.ticket-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 0.6rem;
border-top: 1px dashed #f1f5f9;
margin-top: 0.4rem;
}
.status-badge {
padding: 0.25rem 0.7rem;
border-radius: 50px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.status-open { background: var(--success-gradient); color: white; }
.status-closed{ background: var(--danger-gradient); color: white; }
.chat-btn {
background: var(--primary-gradient);
color: white;
border: none;
padding: 0.45rem 0.9rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 0.4rem;
text-decoration: none;
transition: var(--transition);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25);
}
.chat-btn:hover {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.35);
color: white;
}
.chat-btn::after {
content: '→';
transition: margin-left 0.3s ease;
margin-left: 0;
}
.chat-btn:hover::after {
margin-left: 0.4rem;
}
.empty-state {
text-align: center;
padding: 2.5rem 1.5rem;
background: rgba(255, 255, 255, 0.6);
border-radius: var(--border-radius);
border: 2px dashed #e2e8f0;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
display: block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-15px); }
}
.empty-title {
font-size: 1.4rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.75rem;
}
.empty-subtitle {
color: #64748b;
font-size: 1rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.ticket-id {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
padding: 0.2rem 0.75rem;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.new-message-dot {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 10px;
height: 10px;
background: #ef4444;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
animation: blink 1.5s infinite;
z-index: 10;
}
@keyframes blink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.15); }
}
@media (max-width: 768px) {
.chat-dashboard {
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.ticket-header {
flex-direction: column;
gap: 0.75rem;
}
.ticket-footer {
flex-direction: column;
gap: 0.75rem;
align-items: stretch;
}
.chat-btn {
justify-content: center;
}
}
.tickets-list {
max-height: 55vh;
overflow-y: auto;
padding-right: 0.4rem;
}
.tickets-list::-webkit-scrollbar {
width: 5px;
}
.tickets-list::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 8px;
}
.tickets-list::-webkit-scrollbar-thumb {
background: var(--primary-gradient);
border-radius: 8px;
}
</style>
<div class="chat-dashboard">
<!-- Header -->
<div class="dashboard-header">
<h1 class="dashboard-title">Live Chat Dashboard</h1>
<p class="dashboard-subtitle">
Monitor customer conversations with real-time updates
</p>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon stat-total">💬</div>
<div class="stat-content">
<h3>{{ $tickets->count() }}</h3>
<p>Total Conversations</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-open">📈</div>
<div class="stat-content">
<h3>{{ $tickets->where('status', 'open')->count() }}</h3>
<p>Active Tickets</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-closed"></div>
<div class="stat-content">
<h3>{{ $tickets->where('status', 'closed')->count() }}</h3>
<p>Resolved Tickets</p>
</div>
</div>
</div>
<!-- Tickets Container -->
<div class="tickets-container">
<div class="tickets-header">
<div>
<h2 class="tickets-title">
📋 Active Conversations
<span class="tickets-count">{{ $tickets->count() }}</span>
</h2>
</div>
</div>
@if($tickets->count() === 0)
<div class="p-4 text-center text-muted">
<h5>No customer chats yet.</h5>
<!-- Empty State -->
<div class="empty-state">
<div class="empty-icon">💬</div>
<h3 class="empty-title">No Active Conversations</h3>
<p class="empty-subtitle">
Customer conversations will appear here with real-time notifications.
</p>
<div class="ticket-id">Ready for support requests</div>
</div>
@else
<ul class="list-group list-group-flush">
<!-- Tickets List -->
<div class="tickets-list">
@foreach($tickets as $ticket)
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
@if($ticket->unread_count > 0)
<div class="new-message-dot"></div>
@endif
<div class="ticket-header">
<div class="ticket-avatar">
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
</div>
<div class="ticket-content">
<div class="ticket-name">
{{ $ticket->user->customer_name ?? $ticket->user->name }}
@if($ticket->unread_count > 0)
<span id="badge-{{ $ticket->id }}" class="unread-count">
{{ $ticket->unread_count }}
</span>
@endif
</div>
@php
// Get last message
$lastMsg = $ticket->messages()->latest()->first();
@endphp
<li class="list-group-item py-3">
<div class="d-flex align-items-center justify-content-between">
<!-- LEFT -->
<div class="d-flex align-items-center gap-3">
<!-- Avatar -->
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 45px; height: 45px; font-size: 18px;">
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
</div>
<div>
<!-- Name + unread badge -->
<h6 class="mb-1 fw-semibold">
{{ $ticket->user->customer_name ?? $ticket->user->name }}
<span
id="badge-{{ $ticket->id }}"
class="badge bg-danger ms-2 {{ $ticket->unread_count == 0 ? 'd-none' : '' }}">
{{ $ticket->unread_count }}
</span>
</h6>
<!-- Last message -->
<small class="text-muted">
<div class="ticket-preview">
@if($lastMsg)
@if($lastMsg->message)
{{ Str::limit($lastMsg->message, 35) }}
{{ Str::limit($lastMsg->message, 55) }}
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
📷 Image
📷 Photo shared
@else
📎 Attachment
📎 File attached
@endif
@else
<i>No messages yet</i>
<em>Conversation started</em>
@endif
</small>
</div>
@if($lastMsg)
<div class="ticket-time">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
</svg>
{{ $lastMsg->created_at->diffForHumans() }}
</div>
@endif
</div>
<div class="ticket-id">#{{ $ticket->id }}</div>
</div>
<!-- RIGHT -->
<div class="text-end">
<span class="badge {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
<div class="ticket-footer">
<span class="status-badge status-{{ $ticket->status }}">
{{ ucfirst($ticket->status) }}
</span>
<a href="{{ route('admin.chat.open', $ticket->id) }}"
class="btn btn-sm btn-primary ms-2">
Open Chat
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
Open Chat
</a>
</div>
</div>
</li>
@endforeach
</ul>
@endif
</div>
@endif
</div>
</div>
@endsection
@section('scripts')
<script>
// -------------------------------
@@ -135,7 +644,3 @@ waitForEcho(() => {
});
</script>
@endsection
@endsection

File diff suppressed because it is too large Load Diff

View File

@@ -333,6 +333,13 @@ body, .container-fluid {
border-color: #f59e0b !important;
}
.badge-loading {
background: linear-gradient(135deg, #e3f2fd, #90caf9) !important;
color: #1565c0 !important;
border-color: #2196f3 !important;
width: 110px;
}
/* In Transit Status - SAME SIZE WITH TRUCK ICON */
.badge-in_transit {
background: linear-gradient(135deg, #dbeafe, #93c5fd) !important;
@@ -1101,6 +1108,7 @@ body, .container-fluid {
break-inside: avoid;
}
}
</style>
<div class="container-fluid py-3">

View File

@@ -13,35 +13,41 @@
<div class="d-flex justify-content-between align-items-start">
<div>
<h4 class="fw-bold mb-0">Order Details</h4>
@php
$status = strtolower($order->status ?? '');
@endphp
<small class="text-muted">Detailed view of this shipment order</small>
</div>
{{-- ADD ITEM --}}
@can('order.create')
@if($status === 'pending')
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
<i class="fas fa-plus-circle me-2"></i>Add New Item
</button>
@endif
@endcan
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
</div>
<!-- {{-- ACTION BUTTONS --}}
<div class="mt-3 d-flex gap-2">-->
<div class="mt-3 d-flex gap-2">
{{-- EDIT ORDER --}} -->
<!-- @if($order->status === 'pending')
<button class="btn btn-edit-order" onclick="document.getElementById('editOrderForm').style.display='block'">
{{-- Edit Order --}}
@if($status === 'pending')
<button class="btn btn-edit-order"
onclick="document.getElementById('editOrderForm').style.display='block'">
<i class="fas fa-edit me-2"></i>Edit Order
</button>
@else
<button class="btn btn-edit-order" disabled><i class="fas fa-edit me-2"></i>Edit Order</button>
@endif -->
@endif
<!-- {{-- DELETE ORDER --}}
@if($order->status === 'pending')
{{-- Delete Order --}}
@if($status === 'pending')
<form action="{{ route('admin.orders.destroy', $order->id) }}"
method="POST"
onsubmit="return confirm('Delete this entire order?')">
@@ -51,9 +57,10 @@
<i class="fas fa-trash-alt me-2"></i>Delete Order
</button>
</form>
@endif -->
@endif
</div>
<!-- </div> -->
<hr>
@@ -191,11 +198,10 @@
<td>{{ $item->shop_no }}</td>
<td class="d-flex justify-content-center gap-2">
@if($status === 'pending')
{{-- EDIT BUTTON --}}
@can('order.edit')
<button
type="button"
<button type="button"
class="btn btn-sm btn-edit-item"
data-bs-toggle="modal"
data-bs-target="#editItemModal{{ $item->id }}">
@@ -203,20 +209,22 @@
</button>
@endcan
@can('order.delete')
{{-- DELETE BUTTON --}}
@can('order.delete')
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
method="POST"
onsubmit="return confirm('Delete this item?')">
@csrf
@method('DELETE')
<button class="btn btn-sm btn-delete-item">
<button type="submit" class="btn btn-sm btn-delete-item">
<i class="fas fa-trash"></i>
</button>
</form>
@endcan
@endif
</td>
</tr>
@endforeach
</tbody>
@@ -617,7 +625,7 @@ function fillFormFromDeleted(item) {
box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
margin-right: -800px;
margin-right: -650px;
}
.btn-add-item:hover {

View File

@@ -1297,9 +1297,10 @@
</td>
<td>{{ \Carbon\Carbon::parse($ship->shipment_date)->format('d M Y') }}</td>
<td>
<button type="button" class="btn-eye" onclick="openShipmentDetails({{ $ship->id }})" title="View Shipment">
<a href="{{ route('admin.shipments.dummy', $ship->id) }}"
class="btn-view-details">
<i class="bi bi-eye"></i>
</button>
</a>
</td>
<td>
@@ -1596,7 +1597,7 @@ function renderTable() {
<td>${new Date(shipment.shipment_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
<td>
<a href="{{ route('admin.shipments.dummy', $ship->id) }}"
<a href="/admin/shipment/dummy/${shipment.id}"
class="btn-view-details">
<i class="bi bi-eye"></i>
</a>
@@ -1665,6 +1666,8 @@ function openShipmentDetails(id) {
.then(data => {
// Format date properly
const shipmentDate = new Date(data.shipment.shipment_date);
// <div class="shipment-info-value">${data.shipment.shipment_id}</div>
const formattedDate = shipmentDate.toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +1,896 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('page-title', 'Edit Staff')
@section('content')
<style>
.card { background:#fff; border:1px solid #eaeaea; padding:1rem; border-radius:6px; }
.field { margin-bottom:.8rem; }
label { display:block; font-weight:600; margin-bottom:.25rem; }
input[type="text"], input[type="email"], input[type="date"], input[type="password"], textarea, select {
width:100%; padding:.5rem; border:1px solid #ddd; border-radius:4px;
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--glass-bg: rgba(255, 255, 255, 0.98);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 15px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Compact Container */
.staff-edit-container {
max-width: 900px;
margin: 0 auto;
position: relative;
}
.staff-edit-card {
background: var(--glass-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: var(--shadow-lg);
overflow: hidden;
position: relative;
}
.staff-edit-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--primary-gradient);
z-index: 10;
}
/* Compact Header */
.staff-header {
background: var(--primary-gradient);
padding: 25px;
color: white;
text-align: center;
position: relative;
}
.staff-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: white;
margin: 0 auto 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #667eea;
border: 3px solid white;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.staff-avatar-initials {
font-size: 32px;
font-weight: 700;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.staff-title {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 8px;
}
.staff-subtitle {
font-size: 1rem;
opacity: 0.9;
margin-bottom: 10px;
}
.employee-id-badge {
display: inline-block;
background: rgba(255,255,255,0.15);
padding: 6px 15px;
border-radius: 20px;
font-family: 'Courier New', monospace;
font-weight: 600;
font-size: 0.9rem;
backdrop-filter: blur(5px);
border: 1px solid rgba(255,255,255,0.2);
}
/* Compact Form Sections */
.form-section {
padding: 25px;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.section-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--primary-gradient);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3);
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
color: #2d3748;
margin: 0;
}
/* Compact Form Grid */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.form-group {
margin-bottom: 0;
}
.form-label {
display: block;
font-weight: 600;
color: #4a5568;
margin-bottom: 6px;
font-size: 0.9rem;
}
.required::after {
content: '*';
color: #e53e3e;
margin-left: 3px;
}
/* Compact Form Inputs */
.form-input {
width: 100%;
padding: 12px 16px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
font-size: 14px;
transition: all 0.2s ease;
background: white;
color: #2d3748;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-input:disabled {
background: #f7fafc;
color: #a0aec0;
}
.form-textarea {
min-height: 80px;
resize: vertical;
line-height: 1.4;
}
/* Compact Status Toggle */
.status-toggle {
display: flex;
gap: 10px;
}
.status-option {
flex: 1;
text-align: center;
padding: 12px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
font-size: 0.9rem;
background: white;
}
.status-option:hover {
transform: translateY(-1px);
}
.status-option.active {
border-color: #48bb78;
background: #f0fff4;
color: #276749;
}
.status-option.inactive {
border-color: #fc8181;
background: #fff5f5;
color: #9b2c2c;
}
input[type="radio"] {
display: none;
}
/* Compact Permissions Section */
.permissions-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 15px;
margin-top: 15px;
}
.permission-group {
background: white;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
}
.permission-group:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.1);
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 1px solid #edf2f7;
}
.group-title {
font-size: 1.1rem;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.toggle-group-btn {
background: var(--primary-gradient);
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
font-size: 0.85rem;
transition: all 0.2s ease;
}
.toggle-group-btn:hover {
transform: scale(1.03);
}
.permission-items {
display: flex;
flex-direction: column;
gap: 10px;
}
.permission-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #f8fafc;
border-radius: 8px;
transition: all 0.2s ease;
}
.permission-item:hover {
background: #edf2f7;
transform: translateX(3px);
}
.permission-checkbox {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #cbd5e0;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
flex-shrink: 0;
}
.permission-checkbox:checked {
background: #667eea;
border-color: #667eea;
}
.permission-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: bold;
font-size: 12px;
}
.permission-name {
font-weight: 500;
color: #4a5568;
font-size: 0.9rem;
flex: 1;
}
/* Compact Error Messages */
.error-container {
background: linear-gradient(135deg, #fff5f5, #fed7d7);
border: 1.5px solid #fc8181;
border-radius: 12px;
padding: 20px;
margin: 0 25px 20px 25px;
}
.error-title {
color: #c53030;
font-weight: 600;
font-size: 1rem;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.error-list {
list-style: none;
padding: 0;
margin: 0;
}
.error-item {
color: #742a2a;
padding: 6px 0;
padding-left: 20px;
position: relative;
font-size: 0.9rem;
}
.error-item::before {
content: '⚠️';
position: absolute;
left: 0;
font-size: 0.9rem;
}
/* Compact Action Buttons */
.action-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 25px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.btn {
padding: 12px 24px;
border-radius: 10px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
min-width: 120px;
}
.btn-cancel {
background: white;
color: #4a5568;
border: 1.5px solid #e2e8f0;
}
.btn-cancel:hover {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
.btn-update {
background: var(--primary-gradient);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.btn-update:hover {
transform: translateY(-1px) scale(1.02);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
}
/* Password Strength Indicator */
.password-strength {
margin-top: 6px;
height: 3px;
border-radius: 1.5px;
background: #e2e8f0;
overflow: hidden;
}
.strength-bar {
height: 100%;
width: 0%;
border-radius: 1.5px;
transition: all 0.2s ease;
}
.strength-weak { background: #e53e3e; }
.strength-fair { background: #ed8936; }
.strength-good { background: #38b2ac; }
.strength-strong { background: #48bb78; }
/* Responsive Design */
@media (max-width: 768px) {
body {
padding: 10px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.permissions-container {
grid-template-columns: 1fr;
gap: 12px;
}
.staff-header {
padding: 20px 15px;
}
.staff-title {
font-size: 1.5rem;
}
.form-section {
padding: 20px;
}
.action-buttons {
padding: 20px;
flex-direction: column;
}
.btn {
width: 100%;
}
.error-container {
margin: 0 15px 15px 15px;
padding: 15px;
}
}
@media (max-width: 480px) {
.staff-avatar {
width: 60px;
height: 60px;
font-size: 24px;
}
.staff-avatar-initials {
font-size: 24px;
}
.section-icon {
width: 35px;
height: 35px;
font-size: 14px;
}
.section-title {
font-size: 1.1rem;
}
.permission-group {
padding: 15px;
}
}
.perm-group { border:1px dashed #eee; padding:.6rem; margin-bottom:.6rem; border-radius:4px; }
.perm-list { display:flex; flex-wrap:wrap; gap:.5rem; }
.perm-item { min-width:200px; }
.btn { padding:.5rem .8rem; border-radius:5px; border:1px solid #ccc; background:#f6f6f6; cursor:pointer; }
.btn.primary { background:#0b74de; color:#fff; border-color:#0b74de; }
</style>
<div class="card">
<h3>Edit Staff {{ $staff->display_name ?? $staff->name }}</h3>
<div class="staff-edit-container">
<!-- Main Card -->
<div class="staff-edit-card">
<!-- Compact Header -->
<div class="staff-header">
<div class="staff-avatar">
<div class="staff-avatar-initials">
{{ strtoupper(substr($staff->name, 0, 2)) }}
</div>
</div>
<h1 class="staff-title">Edit Staff Profile</h1>
<div class="staff-subtitle">{{ $staff->name }}</div>
<div class="employee-id-badge">{{ $staff->employee_id }}</div>
</div>
<!-- Error Messages -->
@if($errors->any())
<div style="background:#fff0f0; padding:.6rem; border:1px solid #f3c6c6; margin-bottom:1rem;">
<strong>There were some problems with your input:</strong>
<ul>
<div class="error-container">
<div class="error-title">
<span>⚠️</span>
There were some problems with your input
</div>
<ul class="error-list">
@foreach($errors->all() as $err)
<li>{{ $err }}</li>
<li class="error-item">{{ $err }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.staff.update', $staff->id) }}">
<form method="POST" action="{{ route('admin.staff.update', $staff->id) }}" id="staffForm">
@csrf
@method('PUT')
<div class="field">
<label>Employee ID</label>
<input type="text" value="{{ $staff->employee_id }}" disabled>
<!-- Personal Information Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">👤</div>
<h2 class="section-title">Personal Information</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label required">Full Name</label>
<input type="text"
name="name"
value="{{ old('name', $staff->name) }}"
class="form-input"
placeholder="Enter full name"
required>
</div>
<div class="field">
<label>Name *</label>
<input type="text" name="name" value="{{ old('name', $staff->name) }}" required>
<div class="form-group">
<label class="form-label required">Email</label>
<input type="email"
name="email"
value="{{ old('email', $staff->email) }}"
class="form-input"
placeholder="staff@company.com"
required>
</div>
<div class="field">
<label>Email *</label>
<input type="email" name="email" value="{{ old('email', $staff->email) }}" required>
<div class="form-group">
<label class="form-label required">Phone</label>
<input type="text"
name="phone"
value="{{ old('phone', $staff->phone) }}"
class="form-input"
placeholder="+91 9876543210"
required>
</div>
<div class="field">
<label>Phone *</label>
<input type="text" name="phone" value="{{ old('phone', $staff->phone) }}" required>
<div class="form-group">
<label class="form-label">Emergency Contact</label>
<input type="text"
name="emergency_phone"
value="{{ old('emergency_phone', $staff->emergency_phone) }}"
class="form-input"
placeholder="Emergency contact">
</div>
<div class="field">
<label>Emergency Phone</label>
<input type="text" name="emergency_phone" value="{{ old('emergency_phone', $staff->emergency_phone) }}">
<div class="form-group" style="grid-column: span 2;">
<label class="form-label">Address</label>
<textarea name="address"
class="form-input form-textarea"
placeholder="Enter complete address"
rows="3">{{ old('address', $staff->address) }}</textarea>
</div>
</div>
</div>
<div class="field">
<label>Address</label>
<textarea name="address" rows="3">{{ old('address', $staff->address) }}</textarea>
<!-- Professional Information Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">💼</div>
<h2 class="section-title">Professional Information</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Employee ID</label>
<input type="text"
value="{{ $staff->employee_id }}"
class="form-input"
disabled
style="background: #f7fafc;">
</div>
<hr>
<div class="field">
<label>Role</label>
<input type="text" name="role" value="{{ old('role', $staff->role) }}">
<div class="form-group">
<label class="form-label">Role</label>
<input type="text"
name="role"
value="{{ old('role', $staff->role) }}"
class="form-input"
placeholder="e.g., Manager, Executive">
</div>
<div class="field">
<label>Department</label>
<input type="text" name="department" value="{{ old('department', $staff->department) }}">
<div class="form-group">
<label class="form-label">Department</label>
<input type="text"
name="department"
value="{{ old('department', $staff->department) }}"
class="form-input"
placeholder="e.g., IT, HR">
</div>
<div class="field">
<label>Designation</label>
<input type="text" name="designation" value="{{ old('designation', $staff->designation) }}">
<div class="form-group">
<label class="form-label">Designation</label>
<input type="text"
name="designation"
value="{{ old('designation', $staff->designation) }}"
class="form-input"
placeholder="e.g., Senior Developer">
</div>
<div class="field">
<label>Joining Date</label>
<input type="date" name="joining_date" value="{{ old('joining_date', optional($staff->joining_date)->format('Y-m-d')) }}">
<div class="form-group">
<label class="form-label">Joining Date</label>
<input type="date"
name="joining_date"
value="{{ old('joining_date', optional($staff->joining_date)->format('Y-m-d')) }}"
class="form-input">
</div>
<div class="field">
<label>Status</label>
<select name="status">
<option value="active" {{ old('status', $staff->status)=='active'?'selected':'' }}>Active</option>
<option value="inactive" {{ old('status', $staff->status)=='inactive'?'selected':'' }}>Inactive</option>
</select>
<div class="form-group">
<label class="form-label">Status</label>
<div class="status-toggle">
<label class="status-option {{ old('status', $staff->status) == 'active' ? 'active' : '' }}">
<input type="radio" name="status" value="active" {{ old('status', $staff->status) == 'active' ? 'checked' : '' }}>
<span> Active</span>
</label>
<label class="status-option {{ old('status', $staff->status) == 'inactive' ? 'inactive' : '' }}">
<input type="radio" name="status" value="inactive" {{ old('status', $staff->status) == 'inactive' ? 'checked' : '' }}>
<span>⏸️ Inactive</span>
</label>
</div>
</div>
</div>
</div>
<div class="field">
<label>Additional Info</label>
<textarea name="additional_info" rows="3">{{ old('additional_info', $staff->additional_info) }}</textarea>
<!-- Account Information Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">🔐</div>
<h2 class="section-title">Account Information</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label required">Username</label>
<input type="text"
name="username"
value="{{ old('username', $staff->username) }}"
class="form-input"
placeholder="Choose username"
required>
</div>
<hr>
<div class="field">
<label>Username *</label>
<input type="text" name="username" value="{{ old('username', $staff->username) }}" required>
<div class="form-group">
<label class="form-label">New Password</label>
<input type="password"
name="password"
class="form-input"
placeholder="Leave blank to keep existing"
id="passwordInput"
autocomplete="new-password">
<div class="password-strength">
<div class="strength-bar" id="strengthBar"></div>
</div>
</div>
<div class="field">
<label>New Password (leave blank to keep existing)</label>
<input type="password" name="password" autocomplete="new-password">
<div class="form-group">
<label class="form-label">Confirm Password</label>
<input type="password"
name="password_confirmation"
class="form-input"
placeholder="Confirm new password">
</div>
</div>
</div>
<hr>
<!-- Additional Information Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">📝</div>
<h2 class="section-title">Additional Information</h2>
</div>
<div class="form-grid">
<div class="form-group" style="grid-column: span 2;">
<label class="form-label">Additional Notes</label>
<textarea name="additional_info"
class="form-input form-textarea"
placeholder="Any additional information"
rows="3">{{ old('additional_info', $staff->additional_info) }}</textarea>
</div>
</div>
</div>
<div style="margin-bottom:.5rem; font-weight:700;">Permissions</div>
<!-- Permissions Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">🔑</div>
<h2 class="section-title">Permissions & Access</h2>
</div>
<div class="permissions-container">
@foreach($permissions as $group => $groupPerms)
<div class="perm-group">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:600;">{{ ucfirst($group) }}</div>
<div>
<button type="button" class="btn" onclick="toggleGroup('{{ $group }}')">Toggle group</button>
<div class="permission-group">
<div class="group-header">
<h3 class="group-title">{{ ucfirst($group) }}</h3>
<button type="button"
class="toggle-group-btn"
onclick="toggleGroupPermissions('{{ $group }}')">
Toggle All
</button>
</div>
</div>
<div class="perm-list" id="group-{{ $group }}">
<div class="permission-items" id="permission-group-{{ $group }}">
@foreach($groupPerms as $perm)
<label class="perm-item">
<input type="checkbox" name="permissions[]" value="{{ $perm->name }}"
<label class="permission-item">
<input type="checkbox"
name="permissions[]"
value="{{ $perm->name }}"
class="permission-checkbox"
id="perm-{{ $perm->id }}"
{{ in_array($perm->name, old('permissions', $staffPermissions)) ? 'checked' : '' }}>
{{ $perm->name }}
<span class="permission-name">{{ $perm->name }}</span>
</label>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:.5rem;">
<a href="{{ route('admin.staff.index') }}" class="btn">Cancel</a>
<button type="submit" class="btn primary">Update Staff</button>
<!-- Action Buttons -->
<div class="action-buttons">
<a href="{{ route('admin.staff.index') }}" class="btn btn-cancel">
<span></span>
Cancel
</a>
<button type="submit" class="btn btn-update">
<span>💾</span>
Update Profile
</button>
</div>
</form>
</div>
</div>
<script>
function toggleGroup(group){
const el = document.getElementById('group-'+group);
if(!el) return;
const inputs = el.querySelectorAll('input[type="checkbox"]');
const anyUnchecked = Array.from(inputs).some(i => !i.checked);
inputs.forEach(i => i.checked = anyUnchecked);
// Password Strength Indicator
document.getElementById('passwordInput').addEventListener('input', function(e) {
const password = e.target.value;
const strengthBar = document.getElementById('strengthBar');
let strength = 0;
if (password.length >= 8) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
let width = 0;
let className = '';
switch(strength) {
case 0:
width = 0;
className = 'strength-weak';
break;
case 1:
width = 25;
className = 'strength-weak';
break;
case 2:
width = 50;
className = 'strength-fair';
break;
case 3:
width = 75;
className = 'strength-good';
break;
case 4:
width = 100;
className = 'strength-strong';
break;
}
strengthBar.style.width = width + '%';
strengthBar.className = 'strength-bar ' + className;
});
// Toggle Group Permissions
function toggleGroupPermissions(group) {
const checkboxes = document.querySelectorAll(`#permission-group-${group} .permission-checkbox`);
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
checkbox.dispatchEvent(new Event('change'));
});
}
// Status toggle selection
document.querySelectorAll('.status-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.status-option').forEach(opt => {
opt.classList.remove('active', 'inactive');
});
const input = this.querySelector('input');
if (input.value === 'active') {
this.classList.add('active');
} else {
this.classList.add('inactive');
}
input.checked = true;
});
});
// Form validation
document.getElementById('staffForm').addEventListener('submit', function(e) {
const requiredFields = this.querySelectorAll('[required]');
let valid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
valid = false;
field.style.borderColor = '#e53e3e';
field.style.animation = 'shake 0.3s ease';
setTimeout(() => {
field.style.animation = '';
}, 300);
} else {
field.style.borderColor = '#e2e8f0';
}
});
if (!valid) {
e.preventDefault();
const firstError = this.querySelector('[required]:invalid');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
});
// Add shake animation
const style = document.createElement('style');
style.textContent = `
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
`;
document.head.appendChild(style);
</script>
@endsection

View File

@@ -1,30 +1,581 @@
@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>
<div class="container-fluid py-4">
<div class="card">
@if(session('success'))
<div style="padding:.5rem; background:#e6ffed; border:1px solid #b6f0c6; margin-bottom:1rem;">{{ session('success') }}</div>
<div class="alert-success">
{{ session('success') }}
</div>
@endif
<table>
{{-- 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>
@@ -37,29 +588,309 @@
<th>Actions</th>
</tr>
</thead>
<tbody>
<tbody id="staffTableBody">
@php
$totalStaff = count($staff);
@endphp
@forelse($staff as $s)
<tr>
<td>{{ $s->id }}</td>
<td class="muted">{{ $s->employee_id }}</td>
<td>{{ $s->name }}</td>
<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>{{ $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?')">
<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 class="btn" type="submit">Delete</button>
<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="muted">No staff found.</td></tr>
<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>
<script>
// Pagination state
let currentPage = 1;
const itemsPerPage = 10;
let allStaff = @json($staff);
let filteredStaff = [...allStaff];
// 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

File diff suppressed because it is too large Load Diff

View File

@@ -179,6 +179,16 @@ Route::prefix('admin')
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// web.php
Route::delete('/shipments/{shipment}/orders/{order}',
[ShipmentController::class, 'removeOrder']
)->name('admin.shipments.removeOrder');
Route::post('/shipments/{shipment}/add-orders',
[ShipmentController::class, 'addOrders']
)->name('admin.shipments.addOrders');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// ---------------------------
// INVOICES
@@ -207,8 +217,8 @@ Route::prefix('admin')
->name('admin.invoice.installment.delete');
// //Add New Invoice
// Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
//Add New Invoice
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
// ---------------------------