1562 lines
63 KiB
PHP
1562 lines
63 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Professional Invoice</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
:root {
|
||
--primary: #0f172a;
|
||
--secondary: #3b82f6;
|
||
--accent: #f59e0b;
|
||
--success: #10b981;
|
||
--danger: #ef4444;
|
||
--warning: #f59e0b;
|
||
--light: #f8fafc;
|
||
--surface: #ffffff;
|
||
--surface2: #f1f5f9;
|
||
--border: #e2e8f0;
|
||
--text-muted: #64748b;
|
||
--text-secondary: #475569;
|
||
--radius: 12px;
|
||
--radius-sm: 8px;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||
background: linear-gradient(135deg, #e0f2fe 0%, #f0fdf4 50%, #fef9c3 100%);
|
||
min-height: 100vh;
|
||
color: var(--primary);
|
||
padding: 2rem 1rem;
|
||
}
|
||
|
||
/* ── CONTAINER ── */
|
||
.invoice-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: var(--surface);
|
||
border-radius: 20px;
|
||
box-shadow: var(--shadow-lg);
|
||
overflow: hidden;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* ── TOP ACCENT BAR ── */
|
||
.invoice-accent-bar {
|
||
height: 5px;
|
||
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 40%, #10b981 70%, #f59e0b 100%);
|
||
}
|
||
|
||
/* ── HEADER ── */
|
||
.invoice-header {
|
||
padding: 2rem 2.5rem 1.5rem;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
position: relative;
|
||
}
|
||
|
||
.invoice-title {
|
||
font-size: 2rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.03em;
|
||
color: var(--primary);
|
||
line-height: 1;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.invoice-number {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
font-size: 0.8rem;
|
||
font-weight: 700;
|
||
padding: 0.5rem 1.1rem;
|
||
border-radius: 50px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.status-paid { background: #dcfce7; color: #166534; }
|
||
.status-overdue { background: #fee2e2; color: #991b1b; }
|
||
.status-pending { background: #fef3c7; color: #92400e; }
|
||
.status-default { background: #f1f5f9; color: #475569; }
|
||
|
||
/* ── ID BOXES ── */
|
||
.id-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
padding: 1.5rem 2.5rem;
|
||
background: var(--surface2);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.id-box {
|
||
background: var(--surface);
|
||
border-radius: var(--radius);
|
||
padding: 1.1rem 1.25rem;
|
||
border: 1px solid var(--border);
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
transition: box-shadow 0.2s;
|
||
}
|
||
|
||
.id-box:hover { box-shadow: var(--shadow); }
|
||
|
||
.id-icon-wrap {
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.id-icon-blue { background: #eff6ff; color: #3b82f6; }
|
||
.id-icon-green { background: #f0fdf4; color: #10b981; }
|
||
|
||
.id-label {
|
||
font-size: 0.68rem;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
margin-bottom: 0.2rem;
|
||
}
|
||
|
||
.id-value {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.92rem;
|
||
font-weight: 600;
|
||
color: var(--primary);
|
||
}
|
||
|
||
/* ── DATES ── */
|
||
.date-strip {
|
||
padding: 1.5rem 2.5rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.date-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.date-card {
|
||
flex: 1;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 1rem 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.85rem;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.date-icon-wrap {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 9px;
|
||
background: #eff6ff;
|
||
color: #3b82f6;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.9rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.date-label {
|
||
font-size: 0.68rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 0.2rem;
|
||
}
|
||
|
||
.date-value {
|
||
font-weight: 700;
|
||
font-size: 0.95rem;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.date-value.overdue { color: #ef4444; }
|
||
|
||
.date-arrow {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
font-size: 0.8rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ── SECTION PANEL ── */
|
||
.panel {
|
||
margin: 1.5rem 2.5rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.panel-header {
|
||
background: var(--surface2);
|
||
padding: 0.85rem 1.25rem;
|
||
border-bottom: 1px solid var(--border);
|
||
font-weight: 700;
|
||
font-size: 0.88rem;
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.panel-header i {
|
||
color: var(--secondary);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 1.25rem;
|
||
background: var(--surface);
|
||
}
|
||
|
||
/* ── CUSTOMER ── */
|
||
.customer-name {
|
||
font-size: 1.05rem;
|
||
font-weight: 700;
|
||
color: var(--secondary);
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.customer-detail {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.3rem;
|
||
display: flex;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.customer-detail strong { color: var(--primary); font-weight: 600; }
|
||
|
||
/* ── TABLE ── */
|
||
.invoice-table {
|
||
width: 100%;
|
||
min-width: 1100px;
|
||
border-collapse: collapse;
|
||
font-size: 0.83rem;
|
||
}
|
||
|
||
/* Items table specifically even wider */
|
||
.items-table {
|
||
min-width: 1300px;
|
||
}
|
||
|
||
.invoice-table thead tr {
|
||
background: var(--surface2);
|
||
}
|
||
|
||
.invoice-table thead th {
|
||
padding: 0.85rem 1rem;
|
||
font-size: 0.71rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--text-muted);
|
||
border-bottom: 2px solid var(--border);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.invoice-table tbody tr {
|
||
border-bottom: 1px solid var(--border);
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.invoice-table tbody tr:hover { background: #f0f7ff; }
|
||
.invoice-table tbody tr:last-child { border-bottom: none; }
|
||
|
||
.invoice-table tbody td {
|
||
padding: 0.85rem 1rem;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Description column can wrap */
|
||
.invoice-table tbody td.desc-col {
|
||
white-space: normal;
|
||
min-width: 180px;
|
||
}
|
||
|
||
.invoice-table tbody tr.grouped-item-row {
|
||
background: #f8f9ff;
|
||
}
|
||
|
||
.badge-shop {
|
||
display: inline-block;
|
||
padding: 0.2rem 0.55rem;
|
||
border-radius: 5px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.price-green { color: #10b981; font-weight: 700; }
|
||
.price-blue { color: #3b82f6; font-weight: 700; }
|
||
|
||
/* ── ACTION BAR ── */
|
||
.action-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.75rem 1.25rem;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--surface2);
|
||
}
|
||
|
||
.action-bar small { color: var(--text-muted); font-size: 0.8rem; }
|
||
.action-bar span { font-weight: 700; color: var(--secondary); }
|
||
|
||
/* ── BUTTONS ── */
|
||
.btn-create-group {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
background: var(--secondary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 0.45rem 1rem;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s, opacity 0.2s;
|
||
}
|
||
|
||
.btn-create-group:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.btn-create-group:not(:disabled):hover { background: #2563eb; }
|
||
|
||
/* ── CHARGE GROUP FORM PANEL ── */
|
||
.cg-panel {
|
||
margin: 0 2.5rem 1.5rem;
|
||
border: 2px solid #3b82f6;
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: 0 0 0 4px rgba(59,130,246,0.08);
|
||
}
|
||
|
||
.cg-panel-header {
|
||
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
|
||
padding: 0.9rem 1.25rem;
|
||
color: white;
|
||
font-weight: 700;
|
||
font-size: 0.9rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.cg-body { padding: 1.25rem; background: var(--surface); }
|
||
|
||
.form-label-custom {
|
||
font-size: 0.72rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 0.35rem;
|
||
display: block;
|
||
}
|
||
|
||
.form-control-custom,
|
||
.form-select-custom {
|
||
width: 100%;
|
||
padding: 0.55rem 0.85rem;
|
||
border: 1.5px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 0.85rem;
|
||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||
color: var(--primary);
|
||
background: var(--surface);
|
||
transition: border-color 0.15s, box-shadow 0.15s;
|
||
outline: none;
|
||
}
|
||
|
||
.form-control-custom:focus,
|
||
.form-select-custom:focus {
|
||
border-color: var(--secondary);
|
||
box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
|
||
}
|
||
|
||
.basis-box {
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 9px;
|
||
padding: 1rem;
|
||
height: 100%;
|
||
}
|
||
|
||
.basis-box-label {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.basis-value-display {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
color: var(--secondary);
|
||
}
|
||
|
||
.basis-label-display {
|
||
font-size: 0.75rem;
|
||
color: var(--text-muted);
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.basis-hint { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.35rem; }
|
||
|
||
.suggested-total {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
color: #10b981;
|
||
}
|
||
|
||
/* ── CHARGE GROUPS TABLE ── */
|
||
.cg-groups-panel {
|
||
margin: 0 2.5rem 1.5rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.cg-groups-header {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 0.85rem 1.25rem;
|
||
font-weight: 700;
|
||
font-size: 0.88rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
/* ── SUMMARY ── */
|
||
.summary-wrap {
|
||
padding: 0 2.5rem 2rem;
|
||
}
|
||
|
||
.summary-card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.summary-header-row {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 0.85rem 1.25rem;
|
||
font-weight: 700;
|
||
font-size: 0.88rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.summary-body { padding: 1.25rem; }
|
||
|
||
.summary-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.6rem 0;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
.summary-row:last-child { border-bottom: none; padding-top: 0.85rem; }
|
||
.summary-row .label { color: var(--text-secondary); font-weight: 500; }
|
||
.summary-row .value { font-weight: 700; color: var(--primary); }
|
||
.summary-row.total .label { font-size: 1rem; font-weight: 700; color: var(--primary); }
|
||
.summary-row.total .value { font-size: 1.15rem; color: #10b981; }
|
||
.summary-row .value.red { color: #ef4444; }
|
||
|
||
/* ── FOOTER ── */
|
||
.invoice-footer {
|
||
padding: 1.5rem 2.5rem;
|
||
border-top: 1px solid var(--border);
|
||
text-align: center;
|
||
background: var(--surface2);
|
||
}
|
||
|
||
.btn-download {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
background: var(--secondary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 9px;
|
||
padding: 0.65rem 1.4rem;
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
transition: background 0.2s;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.btn-download:hover { background: #2563eb; color: white; }
|
||
|
||
.btn-share {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
background: #10b981;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 9px;
|
||
padding: 0.65rem 1.4rem;
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-share:hover { background: #059669; }
|
||
|
||
.footer-note {
|
||
margin-top: 1.25rem;
|
||
color: var(--text-muted);
|
||
font-size: 0.82rem;
|
||
}
|
||
|
||
/* ── CHECKBOX STYLE ── */
|
||
input[type="checkbox"] {
|
||
width: 15px;
|
||
height: 15px;
|
||
accent-color: var(--secondary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ── RESPONSIVE ── */
|
||
@media (max-width: 768px) {
|
||
body { padding: 0.75rem 0.5rem; }
|
||
.invoice-header,
|
||
.date-strip,
|
||
.summary-wrap,
|
||
.invoice-footer { padding-left: 1.25rem; padding-right: 1.25rem; }
|
||
.id-grid { grid-template-columns: 1fr; padding: 1rem 1.25rem; }
|
||
.date-row { flex-direction: column; }
|
||
.panel, .cg-panel, .cg-groups-panel { margin-left: 1.25rem; margin-right: 1.25rem; }
|
||
.invoice-table { font-size: 0.75rem; }
|
||
}
|
||
|
||
/* ── GROUP ITEMS HIDDEN ROW ── */
|
||
.cg-items-row { background: #f8faff; }
|
||
.cg-toggle-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
padding: 0.3rem 0.75rem;
|
||
border: 1.5px solid #3b82f6;
|
||
border-radius: 6px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: #3b82f6;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.cg-toggle-btn:hover { background: #eff6ff; }
|
||
|
||
/* ── DIVIDER ── */
|
||
.section-divider { height: 1px; background: var(--border); margin: 0 2.5rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="invoice-container">
|
||
|
||
<!-- ACCENT BAR -->
|
||
<div class="invoice-accent-bar"></div>
|
||
|
||
@php
|
||
$showActions = $showActions ?? true;
|
||
@endphp
|
||
|
||
<!-- ═══════════════════════════ HEADER ═══════════════════════════ -->
|
||
<div class="invoice-header">
|
||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
|
||
<div>
|
||
<div class="invoice-title">
|
||
<i class="fas fa-file-invoice" style="color:#3b82f6;font-size:1.5rem;vertical-align:middle;margin-right:0.4rem;"></i>INVOICE
|
||
</div>
|
||
<div class="invoice-number mt-1">{{ $invoice->invoice_number }}</div>
|
||
</div>
|
||
<div>
|
||
@if($invoice->status == 'paid')
|
||
<span class="status-badge status-paid">
|
||
<i class="fas fa-check-circle"></i> Paid
|
||
</span>
|
||
@elseif($invoice->status == 'overdue')
|
||
<span class="status-badge status-overdue">
|
||
<i class="fas fa-exclamation-circle"></i> Overdue
|
||
</span>
|
||
@elseif($invoice->status == 'pending')
|
||
<span class="status-badge status-pending">
|
||
<i class="fas fa-clock"></i> Pending
|
||
</span>
|
||
@else
|
||
<span class="status-badge status-default">
|
||
<i class="fas fa-question-circle"></i> {{ ucfirst($invoice->status) }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ ID BOXES ═══════════════════════════ -->
|
||
<div class="id-grid">
|
||
<!-- Invoice ID -->
|
||
<div class="id-box">
|
||
<div class="id-icon-wrap id-icon-blue">
|
||
<i class="fas fa-receipt"></i>
|
||
</div>
|
||
<div>
|
||
<div class="id-label">Invoice ID</div>
|
||
<div class="id-value">{{ $invoice->invoice_number }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Container ID -->
|
||
<div class="id-box">
|
||
<div class="id-icon-wrap id-icon-green">
|
||
<i class="fas fa-box"></i>
|
||
</div>
|
||
<div>
|
||
<div class="id-label">Container ID</div>
|
||
<div class="id-value">
|
||
@if($invoice->container && $invoice->container->container_number)
|
||
{{ $invoice->container->container_number }}
|
||
@elseif($invoice->container_id)
|
||
{{ $invoice->container_id }}
|
||
@else
|
||
@include('admin.popup_invoice', ['invoice' => $invoice, 'shipment' => $shipment])
|
||
N/A
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ DATES ═══════════════════════════ -->
|
||
<div class="date-strip">
|
||
<div class="date-row">
|
||
<div class="date-card">
|
||
<div class="date-icon-wrap">
|
||
<i class="fas fa-calendar-alt"></i>
|
||
</div>
|
||
<div>
|
||
<div class="date-label">Invoice Date</div>
|
||
<div class="date-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="date-arrow">
|
||
<i class="fas fa-arrow-right"></i>
|
||
</div>
|
||
|
||
<div class="date-card">
|
||
<div class="date-icon-wrap" style="background:#fff7ed;color:#f59e0b;">
|
||
<i class="fas fa-clock"></i>
|
||
</div>
|
||
<div>
|
||
<div class="date-label">Due Date</div>
|
||
<div class="date-value {{ $invoice->status == 'overdue' ? 'overdue' : '' }}">
|
||
{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ CUSTOMER ═══════════════════════════ -->
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<i class="fas fa-user-circle"></i> Customer Details
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="customer-name">{{ $invoice->customer_name }}</div>
|
||
@if($invoice->company_name)
|
||
<div class="customer-detail"><strong>Company:</strong> {{ $invoice->company_name }}</div>
|
||
@endif
|
||
<div class="customer-detail"><strong>Mobile:</strong> {{ $invoice->customer_mobile }}</div>
|
||
<div class="customer-detail"><strong>Email:</strong> {{ $invoice->customer_email }}</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="customer-detail"><strong>Address:</strong></div>
|
||
<div class="customer-detail">{{ $invoice->customer_address }}</div>
|
||
<div class="customer-detail"><strong>Pincode:</strong> {{ $invoice->pincode }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ ITEMS TABLE ═══════════════════════════ -->
|
||
@php
|
||
$isEmbedded = isset($embedded) && $embedded;
|
||
@endphp
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<i class="fas fa-list"></i> Invoice Items
|
||
</div>
|
||
|
||
<!-- @if($isEmbedded)
|
||
<form action="{{ route('admin.invoices.items.update', $invoice->id) }}" method="POST">
|
||
@csrf
|
||
@method('PUT')
|
||
@endif -->
|
||
|
||
<div class="table-responsive">
|
||
<table class="invoice-table items-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-center" style="width:44px;">
|
||
<input type="checkbox" id="selectAllItems">
|
||
</th>
|
||
<th class="text-center" style="min-width:50px;">#</th>
|
||
<th style="min-width:200px;">Description</th>
|
||
<th class="text-center" style="min-width:75px;">CTN</th>
|
||
<th class="text-center" style="min-width:75px;">QTY</th>
|
||
<th class="text-center" style="min-width:95px;">TTL/QTY</th>
|
||
<th class="text-center" style="min-width:75px;">Unit</th>
|
||
<th class="text-center" style="min-width:110px;">Price</th>
|
||
<th class="text-center" style="min-width:125px;">TTL Amount</th>
|
||
<th class="text-center" style="min-width:85px;">CBM</th>
|
||
<th class="text-center" style="min-width:95px;">TTL CBM</th>
|
||
<th class="text-center" style="min-width:80px;">KG</th>
|
||
<th class="text-center" style="min-width:95px;">TTL KG</th>
|
||
<th class="text-center" style="min-width:95px;">Shop No</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach($invoice->items as $i => $item)
|
||
@php
|
||
$alreadyGrouped = in_array($item->id, $groupedItemIds ?? []);
|
||
@endphp
|
||
<tr class="{{ $alreadyGrouped ? 'grouped-item-row' : '' }}">
|
||
<td class="text-center">
|
||
<input type="checkbox"
|
||
class="item-select-checkbox"
|
||
value="{{ $item->id }}"
|
||
{{ $alreadyGrouped ? 'disabled' : '' }}>
|
||
</td>
|
||
<td class="text-center" style="font-weight:600;color:var(--text-muted);">{{ $i + 1 }}</td>
|
||
<td class="desc-col" style="font-weight:600;color:var(--primary);">{{ $item->description }}</td>
|
||
<td class="text-center">{{ $item->ctn }}</td>
|
||
<td class="text-center">{{ $item->qty }}</td>
|
||
<td class="text-center" style="font-weight:700;">{{ $item->ttl_qty }}</td>
|
||
<td class="text-center">{{ $item->unit }}</td>
|
||
|
||
@if($isEmbedded)
|
||
<td class="text-center" style="min-width:120px;">
|
||
<input type="number" step="0.01" min="0"
|
||
name="items[{{ $item->id }}][price]"
|
||
value="{{ old('items.' . $item->id . '.price', $item->price) }}"
|
||
class="form-control form-control-sm text-end">
|
||
</td>
|
||
<td class="text-center" style="min-width:140px;">
|
||
<input type="number" step="0.01" min="0"
|
||
name="items[{{ $item->id }}][ttl_amount]"
|
||
value="{{ old('items.' . $item->id . '.ttl_amount', $item->ttl_amount) }}"
|
||
class="form-control form-control-sm text-end">
|
||
</td>
|
||
@else
|
||
<td class="text-center price-green">₹{{ number_format($item->price, 2) }}</td>
|
||
<td class="text-center price-blue">₹{{ number_format($item->ttl_amount, 2) }}</td>
|
||
@endif
|
||
|
||
<td class="text-center">{{ $item->cbm }}</td>
|
||
<td class="text-center">{{ $item->ttl_cbm }}</td>
|
||
<td class="text-center">{{ $item->kg }}</td>
|
||
<td class="text-center">{{ $item->ttl_kg }}</td>
|
||
<td class="text-center"><span class="badge-shop">{{ $item->shop_no }}</span></td>
|
||
</tr>
|
||
@endforeach
|
||
|
||
@if($invoice->items->isEmpty())
|
||
<tr>
|
||
<td colspan="15" class="text-center py-4" style="color:var(--text-muted);font-weight:600;">
|
||
<i class="fas fa-inbox me-2" style="font-size:1.3rem;opacity:.4;"></i><br>No invoice items found.
|
||
</td>
|
||
</tr>
|
||
@endif
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- ACTION BAR -->
|
||
<div class="action-bar">
|
||
<small>Selected items for charge group: <span id="selectedItemsCount">0</span></small>
|
||
<button type="button" id="btnCreateChargeGroup" class="btn-create-group" disabled>
|
||
<i class="fas fa-layer-group"></i> Create Charge Group
|
||
</button>
|
||
</div>
|
||
|
||
@if($isEmbedded)
|
||
<div class="text-end p-3" style="border-top:1px solid var(--border);">
|
||
<button type="submit" class="btn btn-primary btn-sm">
|
||
<i class="fas fa-save me-1"></i> Update Items & Recalculate
|
||
</button>
|
||
</div>
|
||
</form>
|
||
@endif
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ CHARGE GROUP FORM ═══════════════════════════ -->
|
||
<div id="chargeGroupBox" class="cg-panel d-none">
|
||
<div class="cg-panel-header">
|
||
<i class="fas fa-layer-group"></i> Create Charge Group for Selected Items
|
||
</div>
|
||
<div class="cg-body">
|
||
<form method="POST" id="chargeGroupForm"
|
||
action="{{ route('admin.invoices.charge-group.store', $invoice->id) }}">
|
||
@csrf
|
||
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-md-4">
|
||
<label class="form-label-custom">Group Name</label>
|
||
<input type="text" class="form-control-custom" id="cgGroupName"
|
||
name="group_name" placeholder="e.g. Group #1">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label-custom">Price based on</label>
|
||
<select class="form-select-custom" id="cgBasis">
|
||
<option value="" selected disabled>Select basis</option>
|
||
<option value="ttl_qty">TTL/QTY</option>
|
||
<option value="amount">AMOUNT</option>
|
||
<option value="ttl_cbm">TTL CBM</option>
|
||
<option value="ttl_kg">TTL KG</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label-custom">Rate per selected basis</label>
|
||
<input type="number" step="0.0001" class="form-control-custom"
|
||
id="cgRate" placeholder="Enter rate">
|
||
</div>
|
||
</div>
|
||
|
||
<input type="hidden" name="basis_type" id="cgBasisTypeInput">
|
||
<input type="hidden" name="basis_value" id="cgBasisValueInput">
|
||
<input type="hidden" name="rate" id="cgRateHidden">
|
||
<input type="hidden" name="auto_total" id="cgAutoTotal">
|
||
|
||
<div class="row g-3 mb-3">
|
||
<!-- LEFT: Total basis value -->
|
||
<div class="col-md-6">
|
||
<div class="basis-box">
|
||
<div class="basis-box-label">Total basis value</div>
|
||
<div style="display:flex;align-items:baseline;gap:0.4rem;">
|
||
<span class="basis-value-display" id="cgBasisValue">0</span>
|
||
<span class="basis-label-display" id="cgBasisLabel"></span>
|
||
</div>
|
||
<div class="basis-hint" id="cgBasisHint">
|
||
Select basis to see total TTLQTY, CBM, KG or Amount.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: Total charges (admin input) -->
|
||
<div class="col-md-6">
|
||
<div class="basis-box">
|
||
<div class="basis-box-label">Total charges (admin input)</div>
|
||
<input type="number"
|
||
step="0.01"
|
||
class="form-control-custom"
|
||
id="cgTotalChargeInput"
|
||
name="totalcharge"
|
||
placeholder="Enter total charges"
|
||
readonly>
|
||
<div class="basis-hint" style="margin-top:0.5rem">
|
||
Suggested Rate × basis:
|
||
<span class="suggested-total" id="cgSuggestedTotal">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label-custom" style="margin-bottom:0.5rem;">Selected items in this group</label>
|
||
<div class="table-responsive" style="border:1px solid var(--border);border-radius:9px;overflow:hidden;">
|
||
<table class="invoice-table">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Description</th>
|
||
<th class="text-center">QTY</th>
|
||
<th class="text-center">TTL QTY</th>
|
||
<th class="text-center">CBM</th>
|
||
<th class="text-center">TTL CBM</th>
|
||
<th class="text-center">KG</th>
|
||
<th class="text-center">TTL KG</th>
|
||
<th class="text-center">TTL Amount</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="cgItemsTableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-end gap-2">
|
||
<button type="button" id="cgCancelBtn"
|
||
style="padding:0.5rem 1.1rem;border:1.5px solid var(--border);border-radius:8px;background:transparent;font-size:0.85rem;font-weight:600;cursor:pointer;color:var(--text-secondary);">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" id="cgSaveBtn"
|
||
style="display:inline-flex;align-items:center;gap:0.4rem;padding:0.5rem 1.25rem;border:none;border-radius:8px;background:#10b981;color:white;font-size:0.85rem;font-weight:700;cursor:pointer;">
|
||
<i class="fas fa-save"></i> Save Charge Group
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ CHARGE GROUPS LIST ═══════════════════════════ -->
|
||
@if($invoice->chargeGroups->isNotEmpty())
|
||
<div class="cg-groups-panel">
|
||
<div class="cg-groups-header">
|
||
<i class="fas fa-layer-group"></i> Charge Groups
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="invoice-table">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Group Name</th>
|
||
<th>Basis</th>
|
||
<th class="text-end">Basis Value</th>
|
||
<th class="text-end">Rate</th>
|
||
<th class="text-end">Total Charge</th>
|
||
<th class="text-center">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach($invoice->chargeGroups as $index => $group)
|
||
<tr>
|
||
<td>{{ $index + 1 }}</td>
|
||
<td style="font-weight:600;">{{ $group->group_name ?? 'Group '.$group->id }}</td>
|
||
<td><span style="text-transform:uppercase;font-size:0.75rem;font-weight:700;color:var(--text-muted);">{{ $group->basis_type }}</span></td>
|
||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;font-size:0.82rem;">{{ number_format($group->basis_value, 3) }}</td>
|
||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;font-size:0.82rem;">{{ number_format($group->rate, 2) }}</td>
|
||
<td class="text-end price-blue">₹{{ number_format($group->total_charge, 2) }}</td>
|
||
<td class="text-center">
|
||
<button type="button" class="cg-toggle-btn cg-toggle-items"
|
||
data-group-id="{{ $group->id }}">
|
||
<i class="fas fa-eye"></i> View
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr class="cg-items-row d-none" data-group-id="{{ $group->id }}">
|
||
<td colspan="7">
|
||
<div style="padding:1rem;background:#f8faff;border-radius:8px;margin:0.25rem 0;">
|
||
<div style="font-weight:700;font-size:0.8rem;margin-bottom:0.6rem;color:var(--primary);">Items in this group:</div>
|
||
@if($group->items->isEmpty())
|
||
<div style="color:var(--text-muted);font-size:0.82rem;">No items linked.</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="invoice-table">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Description</th>
|
||
<th class="text-center">QTY</th>
|
||
<th class="text-center">TTL QTY</th>
|
||
<th class="text-center">CBM</th>
|
||
<th class="text-center">TTL CBM</th>
|
||
<th class="text-center">KG</th>
|
||
<th class="text-center">TTL KG</th>
|
||
<th class="text-end">TTL Amount</th>
|
||
<th class="text-end">Rate</th>
|
||
<th class="text-end">Total Charge</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach($group->items as $giIndex => $gi)
|
||
@php
|
||
$it = $gi->item;
|
||
$rate = $group->rate;
|
||
$itemBasis = 0;
|
||
switch ($group->basis_type) {
|
||
case 'ttl_qty': $itemBasis = $it->ttl_qty ?? 0; break;
|
||
case 'amount': $itemBasis = $it->ttl_amount ?? 0; break;
|
||
case 'ttl_cbm': $itemBasis = $it->ttl_cbm ?? 0; break;
|
||
case 'ttl_kg': $itemBasis = $it->ttl_kg ?? 0; break;
|
||
}
|
||
$itemTotal = $itemBasis * $rate;
|
||
@endphp
|
||
<tr>
|
||
<td>{{ $giIndex + 1 }}</td>
|
||
<td>{{ $it->description }}</td>
|
||
<td class="text-center">{{ $it->qty }}</td>
|
||
<td class="text-center">{{ $it->ttl_qty }}</td>
|
||
<td class="text-center">{{ $it->cbm }}</td>
|
||
<td class="text-center">{{ $it->ttl_cbm }}</td>
|
||
<td class="text-center">{{ $it->kg }}</td>
|
||
<td class="text-center">{{ $it->ttl_kg }}</td>
|
||
<td class="text-end">{{ number_format($it->ttl_amount, 2) }}</td>
|
||
<td class="text-end">{{ number_format($rate, 2) }}</td>
|
||
<td class="text-end" style="color:#06b6d4;font-weight:700;">{{ number_format($itemTotal, 2) }}</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
<!-- ═══════════════════════════ SUMMARY ═══════════════════════════ -->
|
||
<div class="summary-wrap">
|
||
<div class="row justify-content-end">
|
||
<div class="col-md-5">
|
||
<div class="summary-card">
|
||
<div class="summary-header-row">
|
||
<i class="fas fa-calculator"></i> Final Summary
|
||
</div>
|
||
<div class="summary-body">
|
||
<div class="summary-row">
|
||
<span class="label">Amount</span>
|
||
<span class="value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||
</div>
|
||
|
||
@if($invoice->tax_type === 'gst')
|
||
<div class="summary-row">
|
||
<span class="label">CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent / 2) }}%)</span>
|
||
<span class="value red">₹{{ number_format($invoice->gst_amount / 2, 2) }}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="label">SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent / 2) }}%)</span>
|
||
<span class="value red">₹{{ number_format($invoice->gst_amount / 2, 2) }}</span>
|
||
</div>
|
||
@elseif($invoice->tax_type === 'igst')
|
||
<div class="summary-row">
|
||
<span class="label">IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%)</span>
|
||
<span class="value red">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||
</div>
|
||
@else
|
||
<div class="summary-row">
|
||
<span class="label">GST ({{ $invoice->gst_percent }}%)</span>
|
||
<span class="value red">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="summary-row total">
|
||
<span class="label">Total Payable</span>
|
||
<span class="value">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════ FOOTER ═══════════════════════════ -->
|
||
<div class="invoice-footer">
|
||
@if($invoice->pdf_path && $showActions)
|
||
<a href="{{ asset($invoice->pdf_path) }}" class="btn-download" download>
|
||
<i class="fas fa-download"></i> Download PDF
|
||
</a>
|
||
<button class="btn-share" onclick="shareInvoice()">
|
||
<i class="fas fa-share"></i> Share
|
||
</button>
|
||
@endif
|
||
<div class="footer-note">
|
||
<p style="margin-bottom:0.25rem;">Thank you for your business!</p>
|
||
<p style="margin:0;">For any inquiries, contact us at support@Kent Logistic</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
function shareInvoice() {
|
||
const shareData = {
|
||
title: "Invoice {{ $invoice->invoice_number }}",
|
||
text: "Sharing invoice {{ $invoice->invoice_number }}",
|
||
url: "{{ asset($invoice->pdf_path) }}"
|
||
};
|
||
if (navigator.share) {
|
||
navigator.share(shareData).catch(() => {});
|
||
} else {
|
||
navigator.clipboard.writeText(shareData.url);
|
||
alert("Link copied! Sharing not supported on this browser.");
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const selectAll = document.getElementById('selectAllItems');
|
||
const itemCheckboxes = document.querySelectorAll('.item-select-checkbox');
|
||
const countSpan = document.getElementById('selectedItemsCount');
|
||
const btnCreate = document.getElementById('btnCreateChargeGroup');
|
||
|
||
const chargeGroupBox = document.getElementById('chargeGroupBox');
|
||
const cgCancelBtn = document.getElementById('cgCancelBtn');
|
||
|
||
const cgItemsTableBody = document.getElementById('cgItemsTableBody');
|
||
const cgBasisSelect = document.getElementById('cgBasis');
|
||
const cgRateInput = document.getElementById('cgRate');
|
||
const cgBasisValueSpan = document.getElementById('cgBasisValue');
|
||
const cgBasisLabelSpan = document.getElementById('cgBasisLabel');
|
||
const cgSuggestedTotalSpan = document.getElementById('cgSuggestedTotal');
|
||
const cgTotalChargeInput = document.getElementById('cgTotalChargeInput');
|
||
|
||
const cgAutoTotalInput = document.getElementById('cgAutoTotal');
|
||
|
||
const cgBasisTypeInput = document.getElementById('cgBasisTypeInput');
|
||
const cgBasisValueInput = document.getElementById('cgBasisValueInput');
|
||
const cgRateHidden = document.getElementById('cgRateHidden');
|
||
const cgForm = document.getElementById('chargeGroupForm');
|
||
|
||
function updateSelectionState() {
|
||
let selectedCount = 0;
|
||
itemCheckboxes.forEach(cb => {
|
||
if (cb.checked && !cb.disabled) selectedCount++;
|
||
});
|
||
if (countSpan) countSpan.textContent = selectedCount;
|
||
if (btnCreate) btnCreate.disabled = (selectedCount === 0);
|
||
if (selectedCount === 0 && chargeGroupBox) {
|
||
chargeGroupBox.classList.add('d-none');
|
||
}
|
||
}
|
||
|
||
function getSelectedItemsDataBasic() {
|
||
const items = [];
|
||
itemCheckboxes.forEach(cb => {
|
||
if (cb.checked && !cb.disabled) {
|
||
const row = cb.closest('tr');
|
||
if (!row) return;
|
||
const cellText = (n) =>
|
||
row.querySelector(`td:nth-child(${n})`)?.textContent.trim() ?? '';
|
||
items.push({
|
||
description: cellText(3),
|
||
qty: cellText(5),
|
||
ttlqty: cellText(6),
|
||
cbm: cellText(10),
|
||
ttlcbm: cellText(11),
|
||
kg: cellText(12),
|
||
ttlkg: cellText(13),
|
||
amount: cellText(9),
|
||
});
|
||
}
|
||
});
|
||
return items;
|
||
}
|
||
|
||
function parseNumber(str) {
|
||
if (!str) return 0;
|
||
const cleaned = str.replace(/[₹,\s]/g, '');
|
||
const val = parseFloat(cleaned);
|
||
return isNaN(val) ? 0 : val;
|
||
}
|
||
|
||
function fillChargeGroupItemsTable() {
|
||
if (!cgItemsTableBody) return;
|
||
const items = getSelectedItemsDataBasic();
|
||
cgItemsTableBody.innerHTML = '';
|
||
items.forEach((it, index) => {
|
||
cgItemsTableBody.insertAdjacentHTML('beforeend', `
|
||
<tr>
|
||
<td>${index + 1}</td>
|
||
<td>${it.description}</td>
|
||
<td class="text-center">${it.qty}</td>
|
||
<td class="text-center">${it.ttlqty}</td>
|
||
<td class="text-center">${it.cbm}</td>
|
||
<td class="text-center">${it.ttlcbm}</td>
|
||
<td class="text-center">${it.kg}</td>
|
||
<td class="text-center">${it.ttlkg}</td>
|
||
<td class="text-center">${it.amount}</td>
|
||
</tr>
|
||
`);
|
||
});
|
||
}
|
||
|
||
function refreshBasisSummaryAndSuggestion() {
|
||
if (!chargeGroupBox || chargeGroupBox.classList.contains('d-none')) return;
|
||
const items = getSelectedItemsDataBasic();
|
||
const basis = cgBasisSelect ? cgBasisSelect.value : '';
|
||
let totalBasis = 0;
|
||
let label = '';
|
||
if (basis === 'ttl_qty') {
|
||
totalBasis = items.reduce((sum, it) => sum + parseNumber(it.ttlqty), 0);
|
||
label = 'Total TTL/QTY';
|
||
} else if (basis === 'amount') {
|
||
totalBasis = items.reduce((sum, it) => sum + parseNumber(it.amount), 0);
|
||
label = 'Total Amount';
|
||
} else if (basis === 'ttl_cbm') {
|
||
totalBasis = items.reduce((sum, it) => sum + parseNumber(it.ttlcbm), 0);
|
||
label = 'Total TTL CBM';
|
||
} else if (basis === 'ttl_kg') {
|
||
totalBasis = items.reduce((sum, it) => sum + parseNumber(it.ttlkg), 0);
|
||
label = 'Total TTL KG';
|
||
}
|
||
if (cgBasisValueSpan) cgBasisValueSpan.textContent = totalBasis ? totalBasis.toFixed(3) : '0';
|
||
if (cgBasisLabelSpan) cgBasisLabelSpan.textContent = label || '–';
|
||
const rate = parseNumber(cgRateInput ? cgRateInput.value : '0');
|
||
const suggested = rate * totalBasis;
|
||
if (cgSuggestedTotalSpan) cgSuggestedTotalSpan.textContent = suggested ? suggested.toFixed(2) : '0';
|
||
if (cgBasisTypeInput) cgBasisTypeInput.value = basis || '';
|
||
if (cgBasisValueInput) cgBasisValueInput.value = totalBasis || 0;
|
||
if (cgRateHidden && cgRateInput) cgRateHidden.value = cgRateInput.value || 0;
|
||
if (cgAutoTotalInput) cgAutoTotalInput.value = suggested || 0;
|
||
if (cgTotalChargeInput) {
|
||
cgTotalChargeInput.value = suggested ? suggested.toFixed(2) : '0';
|
||
}
|
||
}
|
||
|
||
itemCheckboxes.forEach(cb => {
|
||
cb.addEventListener('change', function () {
|
||
updateSelectionState();
|
||
const total = itemCheckboxes.length;
|
||
const checked = Array.from(itemCheckboxes).filter(c => c.checked && !c.disabled).length;
|
||
if (selectAll) {
|
||
selectAll.checked = (checked > 0 && checked === total);
|
||
selectAll.indeterminate = (checked > 0 && checked < total);
|
||
}
|
||
if (!chargeGroupBox.classList.contains('d-none')) {
|
||
fillChargeGroupItemsTable();
|
||
refreshBasisSummaryAndSuggestion();
|
||
}
|
||
});
|
||
});
|
||
|
||
if (selectAll) {
|
||
selectAll.addEventListener('change', function () {
|
||
itemCheckboxes.forEach(cb => { if (!cb.disabled) cb.checked = selectAll.checked; });
|
||
updateSelectionState();
|
||
if (!chargeGroupBox.classList.contains('d-none')) {
|
||
fillChargeGroupItemsTable();
|
||
refreshBasisSummaryAndSuggestion();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (cgBasisSelect) cgBasisSelect.addEventListener('change', refreshBasisSummaryAndSuggestion);
|
||
if (cgRateInput) cgRateInput.addEventListener('input', refreshBasisSummaryAndSuggestion);
|
||
|
||
if (btnCreate && chargeGroupBox) {
|
||
btnCreate.addEventListener('click', function () {
|
||
const hasSelection = Array.from(itemCheckboxes).some(cb => cb.checked && !cb.disabled);
|
||
if (!hasSelection) return;
|
||
chargeGroupBox.classList.remove('d-none');
|
||
fillChargeGroupItemsTable();
|
||
refreshBasisSummaryAndSuggestion();
|
||
chargeGroupBox.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
});
|
||
}
|
||
|
||
if (cgCancelBtn && chargeGroupBox) {
|
||
cgCancelBtn.addEventListener('click', function () {
|
||
chargeGroupBox.classList.add('d-none');
|
||
itemCheckboxes.forEach(cb => { if (!cb.disabled) cb.checked = false; });
|
||
if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
|
||
updateSelectionState();
|
||
});
|
||
}
|
||
|
||
if (cgForm) {
|
||
cgForm.addEventListener('submit', function (e) {
|
||
// Stop normal form submit (no page reload)
|
||
e.preventDefault();
|
||
|
||
// 1) Collect selected item IDs
|
||
const selectedIds = [];
|
||
itemCheckboxes.forEach(cb => {
|
||
if (cb.checked && !cb.disabled) {
|
||
selectedIds.push(cb.value);
|
||
}
|
||
});
|
||
|
||
// 2) Frontend validations (same as before)
|
||
if (selectedIds.length === 0) {
|
||
alert('Please select at least one item for this charge group.');
|
||
return;
|
||
}
|
||
|
||
if (!cgBasisSelect || !cgBasisSelect.value) {
|
||
alert('Please select a basis for this charge group.');
|
||
return;
|
||
}
|
||
|
||
if (
|
||
!cgTotalChargeInput ||
|
||
!cgTotalChargeInput.value ||
|
||
parseFloat(cgTotalChargeInput.value) <= 0
|
||
) {
|
||
alert('Please enter total charges for this group.');
|
||
return;
|
||
}
|
||
|
||
// 3) Remove previously added hidden item_ids[]
|
||
const oldHidden = cgForm.querySelectorAll('input[name="item_ids[]"]');
|
||
oldHidden.forEach(el => el.remove());
|
||
|
||
// 4) Add fresh hidden item_ids[] for current selection
|
||
selectedIds.forEach(id => {
|
||
const input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = 'item_ids[]';
|
||
input.value = id;
|
||
cgForm.appendChild(input);
|
||
});
|
||
|
||
// 5) Build AJAX request
|
||
const url = cgForm.action;
|
||
const formData = new FormData(cgForm);
|
||
|
||
// Optional: disable save button while processing
|
||
if (cgSaveBtn) {
|
||
cgSaveBtn.disabled = true;
|
||
cgSaveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
||
}
|
||
|
||
fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
headers: {
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(res => {
|
||
if (!res.ok) {
|
||
throw new Error('Request failed with status ' + res.status);
|
||
}
|
||
return res.json();
|
||
})
|
||
.then(data => {
|
||
if (!data.success) {
|
||
throw new Error(data.message || 'Failed to create charge group.');
|
||
}
|
||
|
||
const group = data.group;
|
||
|
||
// 6) Append new group main row + hidden items row
|
||
const groupsTbody = document.querySelector(
|
||
'.cg-groups-panel table.invoice-table tbody'
|
||
);
|
||
|
||
if (groupsTbody && group) {
|
||
const currentMainRows = groupsTbody.querySelectorAll(
|
||
'tr:not(.cg-items-row)'
|
||
).length;
|
||
const index = currentMainRows + 1;
|
||
|
||
// Main summary row (same structure as Blade)
|
||
const mainRow = document.createElement('tr');
|
||
mainRow.innerHTML = `
|
||
<td>${index}</td>
|
||
<td style="font-weight:600;">
|
||
${group.group_name ? group.group_name : 'Group #' + group.id}
|
||
</td>
|
||
<td>
|
||
<span style="text-transform:uppercase;font-size:0.75rem;font-weight:700;color:var(--text-muted);">
|
||
${group.basis_type}
|
||
</span>
|
||
</td>
|
||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;font-size:0.82rem;">
|
||
${Number(group.basis_value).toFixed(3)}
|
||
</td>
|
||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;font-size:0.82rem;">
|
||
${Number(group.rate).toFixed(2)}
|
||
</td>
|
||
<td class="text-end price-blue">
|
||
${Number(group.total_charge).toFixed(2)}
|
||
</td>
|
||
<td class="text-center">
|
||
<button type="button"
|
||
class="cg-toggle-btn cg-toggle-items"
|
||
data-group-id="${group.id}">
|
||
<i class="fas fa-eye"></i> View
|
||
</button>
|
||
</td>
|
||
`;
|
||
|
||
// Hidden items row (same idea as Blade .cg-items-row)
|
||
const itemsRow = document.createElement('tr');
|
||
itemsRow.className = 'cg-items-row d-none';
|
||
itemsRow.setAttribute('data-group-id', group.id);
|
||
|
||
let itemsTableHtml = `
|
||
<div style="padding:1rem;background:#f8faff;border-radius:8px;margin:0.25rem 0;">
|
||
<div style="font-weight:700;font-size:0.8rem;margin-bottom:0.6rem;color:var(--primary);">
|
||
Items in this group
|
||
</div>
|
||
`;
|
||
|
||
if (!group.items || group.items.length === 0) {
|
||
itemsTableHtml += `
|
||
<div style="color:var(--text-muted);font-size:0.82rem;">
|
||
No items linked.
|
||
</div>
|
||
`;
|
||
} else {
|
||
itemsTableHtml += `
|
||
<div class="table-responsive">
|
||
<table class="invoice-table">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Description</th>
|
||
<th class="text-center">QTY</th>
|
||
<th class="text-center">TTL QTY</th>
|
||
<th class="text-center">CBM</th>
|
||
<th class="text-center">TTL CBM</th>
|
||
<th class="text-center">KG</th>
|
||
<th class="text-center">TTL KG</th>
|
||
<th class="text-end">TTL Amount</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
group.items.forEach((it, idx) => {
|
||
itemsTableHtml += `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td>${it.description ?? ''}</td>
|
||
<td class="text-center">${it.qty ?? ''}</td>
|
||
<td class="text-center">${it.ttlqty ?? ''}</td>
|
||
<td class="text-center">${it.cbm ?? ''}</td>
|
||
<td class="text-center">${it.ttlcbm ?? ''}</td>
|
||
<td class="text-center">${it.kg ?? ''}</td>
|
||
<td class="text-center">${it.ttlkg ?? ''}</td>
|
||
<td class="text-end">${Number(it.amount ?? 0).toFixed(2)}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
itemsTableHtml += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
itemsTableHtml += `</div>`;
|
||
|
||
itemsRow.innerHTML = `
|
||
<td colspan="7">
|
||
${itemsTableHtml}
|
||
</td>
|
||
`;
|
||
|
||
// Append main row + items row in order
|
||
groupsTbody.appendChild(mainRow);
|
||
groupsTbody.appendChild(itemsRow);
|
||
}
|
||
|
||
// 7) Reset Charge Group UI (hide panel, uncheck items)
|
||
if (chargeGroupBox) {
|
||
chargeGroupBox.classList.add('d-none');
|
||
}
|
||
itemCheckboxes.forEach(cb => {
|
||
if (!cb.disabled) cb.checked = false;
|
||
});
|
||
if (selectAll) {
|
||
selectAll.checked = false;
|
||
selectAll.indeterminate = false;
|
||
}
|
||
updateSelectionState();
|
||
|
||
// Optional: clear form
|
||
if (cgGroupName) cgGroupName.value = '';
|
||
if (cgBasisSelect) cgBasisSelect.value = '';
|
||
if (cgRateInput) cgRateInput.value = '';
|
||
if (cgBasisValueSpan) cgBasisValueSpan.textContent = '0';
|
||
if (cgBasisLabelSpan) cgBasisLabelSpan.textContent = '–';
|
||
if (cgSuggestedTotalSpan) cgSuggestedTotalSpan.textContent = '0';
|
||
if (cgTotalChargeInput) cgTotalChargeInput.value = '';
|
||
})
|
||
|
||
.catch(err => {
|
||
console.error(err);
|
||
alert('Error creating charge group. Please try again.');
|
||
})
|
||
.finally(() => {
|
||
if (cgSaveBtn) {
|
||
cgSaveBtn.disabled = false;
|
||
cgSaveBtn.innerHTML = '<i class="fas fa-save"></i> Save Charge Group';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
updateSelectionState();
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.addEventListener('click', function (e) {
|
||
if (!e.target.classList.contains('cg-toggle-items') &&
|
||
!e.target.closest('.cg-toggle-items')) return;
|
||
const btn = e.target.closest('.cg-toggle-items') || e.target;
|
||
const groupId = btn.getAttribute('data-group-id');
|
||
const row = document.querySelector('.cg-items-row[data-group-id="' + groupId + '"]');
|
||
if (!row) return;
|
||
row.classList.toggle('d-none');
|
||
const isHidden = row.classList.contains('d-none');
|
||
btn.innerHTML = isHidden
|
||
? '<i class="fas fa-eye"></i> View'
|
||
: '<i class="fas fa-eye-slash"></i> Hide';
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |