888 lines
36 KiB
PHP
888 lines
36 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('page-title', 'Account Dashboard')
|
|
|
|
@section('content')
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<style>
|
|
/* ---------- Base ---------- */
|
|
:root{
|
|
--primary-1:#1a2951;
|
|
--primary-2:#243a72;
|
|
--muted:#9ba5bb;
|
|
--card-bg: linear-gradient(180deg,#ffffff,#f5f7fb);
|
|
--glass: rgba(255,255,255,0.6);
|
|
--success:#34c86c;
|
|
--danger:#ef4f4f;
|
|
--accent:#276dea;
|
|
--rounded:12px;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
background: linear-gradient(135deg, #eef2f7, #f9fbff);
|
|
margin:0; padding:0;
|
|
color:#253047;
|
|
-webkit-font-smoothing:antialiased;
|
|
}
|
|
|
|
/* container */
|
|
.account-container {
|
|
padding: 28px 34px;
|
|
max-width:1300px;
|
|
margin: 18px auto;
|
|
box-sizing:border-box;
|
|
}
|
|
|
|
/* header */
|
|
.account-header {
|
|
margin-bottom: 18px;
|
|
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
|
padding: 22px 26px;
|
|
border-radius: var(--rounded);
|
|
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
|
|
color: #fff;
|
|
}
|
|
.account-header h2 { font-size: 26px; font-weight:700; margin:0 0 6px 0; }
|
|
.account-header p { font-size:14px; margin:2px 0; opacity:0.95 }
|
|
|
|
/* top actions row */
|
|
.top-actions {
|
|
display:flex; align-items:center; justify-content:space-between;
|
|
gap:12px; margin:16px 0 20px 0; flex-wrap:wrap;
|
|
}
|
|
.top-actions .left {
|
|
display:flex; gap:12px; align-items:center; flex-wrap:wrap;
|
|
}
|
|
.search-row input {
|
|
width:360px; padding:10px 14px; border-radius:8px; border:1px solid #d6dde9;
|
|
background: #fff; font-size:14px; box-shadow:0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.search-row input:focus{ outline:none; border-color:var(--primary-1); box-shadow:0 6px 18px rgba(26,41,81,0.06);}
|
|
|
|
.btn {
|
|
display:inline-flex; align-items:center; justify-content:center; gap:8px;
|
|
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
|
color:#fff; border:none; padding:10px 16px; border-radius:10px; font-weight:600;
|
|
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
|
}
|
|
.btn.ghost { background: transparent; color:var(--primary-1); border:1px solid #dbe4f5; box-shadow:none; }
|
|
.btn:hover{ transform: translateY(-3px); box-shadow: 0 8px 26px rgba(36,58,114,0.12); }
|
|
|
|
/* account panels */
|
|
.account-panels { display:flex; gap:22px; align-items:flex-start; flex-wrap:wrap; }
|
|
.panel-card {
|
|
background: var(--card-bg); border-radius:12px; box-shadow:0 8px 20px rgba(25,40,80,0.06);
|
|
flex:1; min-width:48%; padding:18px; box-sizing:border-box; overflow-x:auto; transition: transform .12s, box-shadow .12s;
|
|
}
|
|
.panel-card:hover{ transform: translateY(-4px); box-shadow:0 12px 28px rgba(25,40,80,0.08); }
|
|
.panel-title { font-weight:700; font-size:16px; color:var(--primary-1); margin-bottom:12px; display:flex; align-items:center; justify-content:space-between; }
|
|
|
|
/* table */
|
|
table { width:100%; border-collapse:collapse; min-width:720px; font-size:14px; }
|
|
th, td { padding:10px 12px; text-align:left; border-bottom:1px solid #eef3fb; white-space:nowrap; color:#2d3b53; }
|
|
th { background: linear-gradient(90deg,#f6f9ff,#fbfdff); color:#4a5570; font-weight:700; font-size:13px; }
|
|
tr:hover td{ background:#fbfdff; }
|
|
.entry-link{ color:var(--accent); text-decoration:underline; cursor:pointer; font-weight:700; }
|
|
|
|
/* badges */
|
|
.status-badge { display:inline-block; padding:6px 12px; border-radius:20px; color:#fff; font-size:13px; font-weight:600; }
|
|
.status-unpaid{ background:var(--danger); }
|
|
.status-paid{ background:var(--success); }
|
|
.status-loading{ background:#509cf8; }
|
|
.status-dispatched{ background:#20c5c7; }
|
|
.pending-badge-red{ background:var(--danger); }
|
|
.pending-badge-green{ background:var(--success); }
|
|
|
|
/* 3-state toggle */
|
|
.toggle-switch-btn {
|
|
appearance:none;
|
|
-webkit-appearance:none;
|
|
width:60px;
|
|
height:24px;
|
|
background:#f25b5b; /* RED */
|
|
border:2px solid #f25b5b;
|
|
border-radius:999px;
|
|
position:relative;
|
|
outline:none;
|
|
cursor:pointer;
|
|
transition: background .22s, border .22s;
|
|
}
|
|
|
|
.toggle-switch-btn::before{
|
|
content:"";
|
|
width:20px;
|
|
height:20px;
|
|
background:#fff;
|
|
border-radius:50%;
|
|
position:absolute;
|
|
top:1.7px;
|
|
left:2px;
|
|
transition: transform .22s;
|
|
box-shadow:0 2px 8px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
/* YELLOW */
|
|
.toggle-switch-btn.mid {
|
|
background:#f2c94c;
|
|
border-color:#f2c94c;
|
|
}
|
|
.toggle-switch-btn.mid::before {
|
|
transform: translateX(20px);
|
|
}
|
|
|
|
/* GREEN */
|
|
.toggle-switch-btn.checked {
|
|
background:#43d05c;
|
|
border-color:#43d05c;
|
|
}
|
|
.toggle-switch-btn.checked::before {
|
|
transform: translateX(38px);
|
|
}
|
|
|
|
|
|
/* plus button */
|
|
.plus-btn { display:inline-block; width:36px; height:36px; border-radius:10px; background:#fff; color:var(--primary-1); border:1.5px solid #e6edf8; font-size:1.15rem; font-weight:700; text-align:center; line-height:34px; cursor:pointer; transition: transform .12s; }
|
|
.plus-btn:hover{ transform: translateY(-3px); box-shadow:0 8px 16px rgba(33,47,90,0.04); }
|
|
|
|
/* ---------- Expandable Create Order Card (Option D) ---------- */
|
|
.create-card {
|
|
margin: 10px 0 18px 0;
|
|
border-radius: 12px;
|
|
background: linear-gradient(180deg,#ffffff,#fbfdff);
|
|
box-shadow:0 10px 30px rgba(20,40,80,0.04);
|
|
overflow: hidden; transition: max-height .28s ease, padding .22s ease;
|
|
max-height: 0; padding: 0 18px; opacity:0; pointer-events:none;
|
|
}
|
|
.create-card.open { max-height: 1500px; padding: 18px; opacity:1; pointer-events:auto; }
|
|
.create-card .create-inner { display:flex; gap:14px; flex-direction:column; }
|
|
.create-card .create-grid { display:grid; grid-template-columns: 1fr 1fr; gap:12px; align-items:start; }
|
|
.create-card label{ font-weight:700; color:#28384f; margin-bottom:6px; font-size:13px; }
|
|
.input, select { width:100%; padding:10px 12px; border-radius:8px; border:1.3px solid #e3eaf6; background:#fff; font-size:14px; box-sizing:border-box; }
|
|
.create-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:12px; }
|
|
|
|
/* consolidated orders area */
|
|
.consolidate-area {
|
|
background: linear-gradient(90deg,#fbfbff,#f9fcff);
|
|
border:1px solid #eef5ff; padding:10px; border-radius:10px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.6);
|
|
margin-top:8px; overflow:auto;
|
|
}
|
|
.consolidate-toggle { display:flex; gap:10px; align-items:center; margin-bottom:8px; }
|
|
.consolidate-tab-btn { background:#fff; border:1px solid #e6edf9; padding:8px 10px; border-radius:8px; font-weight:700; cursor:pointer; }
|
|
.consolidate-tab-btn.active { background:linear-gradient(90deg,var(--primary-1),var(--primary-2)); color:#fff; border-color:transparent; box-shadow:0 6px 18px rgba(36,58,114,0.08); }
|
|
|
|
/* compact table in consolidate */
|
|
#consolidateOrdersTable th, #consolidateOrdersTable td { padding:8px 9px; font-size:13px; border-bottom:1px solid #f1f6ff; }
|
|
|
|
/* ---------- Entry Details Modal (existing) ---------- */
|
|
.modal-fade1 { position:fixed; inset:0; background:rgba(16,24,50,0.44); display:none; align-items:center; justify-content:center; z-index:1200; padding:18px; }
|
|
.modal-fade1.modal-open { display:flex; }
|
|
.modal-box1 { background:#fff; border-radius:12px; padding:16px 18px; box-shadow:0 14px 40px rgba(18,30,60,0.12); max-width:1100px; width:100%; max-height:92vh; overflow:auto; }
|
|
|
|
/* entry summary cards */
|
|
.entry-summary-cards { display:flex; gap:12px; margin-bottom:14px; flex-wrap:wrap; }
|
|
.entry-summary-card { background:#fbfdff; border:1px solid #eef6ff; padding:12px; border-radius:10px; min-width:160px; box-shadow:0 6px 18px rgba(22,36,72,0.03); }
|
|
.entry-summary-label{ font-size:12px; color:var(--muted); }
|
|
.entry-summary-value{ font-size:18px; font-weight:700; color:#253047; margin-top:6px; }
|
|
|
|
/* installment modal */
|
|
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
|
|
|
|
/* small helpers */
|
|
.helper-note{ font-size:12px; color:#7687a3; margin-top:6px; display:block; }
|
|
.empty-state{ padding:18px; text-align:center; color:#6f7b8f; }
|
|
.kv { font-weight:700; color:#26364f; }
|
|
|
|
/* responsive */
|
|
@media (max-width:980px){
|
|
.create-card .create-grid { grid-template-columns: 1fr; }
|
|
.panel-card { min-width:100%; }
|
|
.search-row input{ width:220px; }
|
|
}
|
|
</style>
|
|
|
|
<div class="account-container">
|
|
<!-- Header -->
|
|
<div class="account-header">
|
|
<h2>Account Dashboard</h2>
|
|
<p>Viewing: All Regions — Workflow: Manage payments and dispatch.</p>
|
|
</div>
|
|
|
|
<!-- Top actions -->
|
|
<div class="top-actions">
|
|
<div class="left">
|
|
<div class="search-row">
|
|
<input type="text" id="main-search" placeholder="Search by Entry No or Description" aria-label="Search entries">
|
|
<button class="btn ghost" id="searchBtn">Search</button>
|
|
</div>
|
|
<div style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
|
<button class="btn" id="toggleCreateBtn" aria-expanded="false">+ Create New Order</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button class="btn" id="refreshBtn">⟳ Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expandable Create Order Card (Option D) -->
|
|
<div class="create-card" id="createCard">
|
|
<div class="create-inner">
|
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
|
<div style="font-size:18px; font-weight:800; color:var(--primary-1)">Create New Order</div>
|
|
<button class="consolidate-tab-btn" id="closeCreateInline" title="Close create form">✕</button>
|
|
</div>
|
|
|
|
<form id="createOrderInlineForm" autocomplete="off">
|
|
<div class="create-grid" style="margin-top:6px;">
|
|
<div>
|
|
<label for="inline_description">Description *</label>
|
|
<input class="input" type="text" id="inline_description" name="description" required placeholder="Short description for consolidated entry">
|
|
</div>
|
|
<div>
|
|
<label for="inline_region">Region *</label>
|
|
<select id="inline_region" name="region" class="input" required>
|
|
<option value="China">China</option>
|
|
<option value="Europe">Europe</option>
|
|
<option value="US">US</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="inline_amount">Total Amount (₹) *</label>
|
|
<input class="input" type="number" id="inline_amount" name="amount" min="0" required>
|
|
</div>
|
|
<div>
|
|
<label for="inline_entry_date">Entry Date *</label>
|
|
<input class="input" type="date" id="inline_entry_date" name="entry_date" value="{{ date('Y-m-d') }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:10px;">
|
|
<div class="consolidate-toggle">
|
|
<button type="button" id="toggleConsolidatedBtn" class="consolidate-tab-btn active">Hide Orders</button>
|
|
<div style="font-weight:700; color:#233063; margin-left:6px;">Consolidated Orders</div>
|
|
<div style="margin-left:auto; font-size:13px; color:var(--muted);">Select orders to include in the consolidated entry</div>
|
|
</div>
|
|
|
|
<div class="consolidate-area" id="consolidateArea">
|
|
<table id="consolidateOrdersTable">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Order ID</th>
|
|
<th>Mark No</th>
|
|
<th>Origin</th>
|
|
<th>Destination</th>
|
|
<th>CTN</th>
|
|
<th>QTY</th>
|
|
<th>TTL/QTY</th>
|
|
<th>Total Amount (₹)</th>
|
|
<th>CBM</th>
|
|
<th>TTL CBM</th>
|
|
<th>KG</th>
|
|
<th>TTL KG</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="consolidateOrdersBody">
|
|
<tr><td colspan="14" class="empty-state">Loading available orders...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="create-actions">
|
|
<button type="button" class="btn ghost" id="cancelCreateInline">Cancel</button>
|
|
<button type="submit" class="btn">Create Order</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="helper-note">Tip: Select orders from the list to include them in this consolidated entry. You can also search orders above.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panels -->
|
|
<div class="account-panels" id="account-panels">
|
|
<div class="panel-card">
|
|
<div class="panel-title">
|
|
<span>Payment Sent to China</span>
|
|
<span style="font-size:13px;color:var(--muted)">Total entries: <span id="entriesCount">0</span></span>
|
|
</div>
|
|
<table id="main-payment-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Entry No</th><th>Date</th><th>Description</th>
|
|
<th>Order Quantity</th><th>Region</th><th>Payment</th><th>Amount</th><th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="paymentTableBody">
|
|
<tr><td colspan="8" class="empty-state">Loading entries...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="panel-card">
|
|
<div class="panel-title">
|
|
<span>Order Dispatch Status</span>
|
|
<span style="font-size:13px;color:var(--muted)">Actions: <strong>+ Add Installment</strong></span>
|
|
</div>
|
|
<table id="main-order-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Entry No</th><th>Date</th><th>Description</th>
|
|
<th>Region</th><th>Amount</th><th>Status</th>
|
|
<th>Pending</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="orderTableBody">
|
|
<tr><td colspan="8" class="empty-state">Loading entries...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ENTRY DETAILS MODAL -->
|
|
<div class="modal-fade1" id="entryDetailsModal">
|
|
<div class="modal-box1 entry-details-modal">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
<div>
|
|
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800">Entry Details — <span id="entryDetailsId">-</span></h2>
|
|
<div style="font-size:13px;color:var(--muted)">Complete view of all installments for this entry.</div>
|
|
</div>
|
|
<div><button class="btn ghost" onclick="closeEntryDetailsModal()">Close</button></div>
|
|
</div>
|
|
|
|
<div class="entry-summary-cards">
|
|
<div class="entry-summary-card">
|
|
<div class="entry-summary-label">Original Amount</div>
|
|
<div class="entry-summary-value" id="originalAmount">-</div>
|
|
</div>
|
|
<div class="entry-summary-card">
|
|
<div class="entry-summary-label">Total Processed</div>
|
|
<div class="entry-summary-value" id="totalProcessed">-</div>
|
|
</div>
|
|
<div class="entry-summary-card">
|
|
<div class="entry-summary-label">Pending Balance</div>
|
|
<div class="entry-summary-value" id="pendingBalance">-</div>
|
|
</div>
|
|
<div class="entry-summary-card">
|
|
<div class="entry-summary-label">Total Installments</div>
|
|
<div class="entry-summary-value" id="totalInstallments">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="entry-installments-table" style="width:100%; border-collapse:collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th>Installment</th>
|
|
<th>Date</th>
|
|
<th>Description</th>
|
|
<th>Region</th>
|
|
<th>Amount</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="installmentsTableBody">
|
|
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
|
|
<div style="display:flex; justify-content: flex-end; gap:12px; margin-top:16px;">
|
|
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">Close</button>
|
|
<button type="button" class="btn" id="addInstallmentFromDetails">+ Add New Installment</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Installment Modal -->
|
|
<div class="modal-fade1" id="installmentModal">
|
|
<div class="modal-box1" style="max-width:720px;">
|
|
<div style="display:flex;align-items:center; justify-content:space-between; margin-bottom:12px;">
|
|
<div style="font-size:18px;font-weight:800;color:#243a72;">+ Add New Installment</div>
|
|
<button class="consolidate-tab-btn" onclick="closeInstallmentModal()">✕</button>
|
|
</div>
|
|
|
|
<div style="font-size:14px;color:#6f7b8f;margin-bottom:14px;">Create a new processing entry for the remaining pending amount</div>
|
|
|
|
<div id="instDetailsRow" style="display:flex; gap:18px; margin-bottom:12px; flex-wrap:wrap;"></div>
|
|
|
|
<form id="installmentForm" autocomplete="off">
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
|
<div>
|
|
<label>Processing Date *</label>
|
|
<input type="date" name="proc_date" required style="width:100%; padding:10px; border-radius:8px; border:1.4px solid #e7eefb;" value="{{ date('Y-m-d') }}">
|
|
</div>
|
|
<div>
|
|
<label>Processing Amount *</label>
|
|
<input type="number" min="1" name="proc_amount" required placeholder="Enter amount" style="width:100%; padding:10px; border-radius:8px; border:1.4px solid #e7eefb;">
|
|
<span class="helper-note">Maximum allowed: <strong id="maxPending">-</strong></span>
|
|
</div>
|
|
<div>
|
|
<label>Status</label>
|
|
<select name="status" required style="width:100%; padding:10px; border-radius:8px; border:1.4px solid #e7eefb;">
|
|
<option>Pending</option><option>Loading</option><option>Packed</option><option>Dispatched</option><option>Delivered</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; justify-content:flex-end; gap:12px; margin-top:14px;">
|
|
<button type="button" class="btn ghost" onclick="closeInstallmentModal()">Cancel</button>
|
|
<button type="submit" class="btn">Create Installment</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/* ---------- Helpers & state ---------- */
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
|
|
function jsonFetch(url, opts = {}) {
|
|
opts.headers = Object.assign({'Content-Type':'application/json','X-CSRF-TOKEN': csrfToken}, opts.headers || {});
|
|
if(opts.body && typeof opts.body !== 'string') opts.body = JSON.stringify(opts.body);
|
|
return fetch(url, opts).then(r => {
|
|
// attempt to parse json even on non-ok to get backend message
|
|
return r.json().catch(() => { throw new Error('Invalid server response'); });
|
|
});
|
|
}
|
|
|
|
let entries = [];
|
|
let availableOrders = [];
|
|
let currentEntry = null;
|
|
|
|
/* small util */
|
|
function escapeHtml(s){ if(s === null || s === undefined) return ''; return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m])); }
|
|
function formatCurrency(v){ return '₹' + Number(v || 0).toLocaleString(undefined, { minimumFractionDigits:0, maximumFractionDigits:2 }); }
|
|
function capitalize(s){ if(!s) return ''; s = String(s); return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
function statusClass(status){
|
|
switch(String(status || '').toLowerCase()){
|
|
case 'unpaid': return 'status-unpaid';
|
|
case 'paid': return 'status-paid';
|
|
case 'loading': return 'status-loading';
|
|
case 'dispatched': return 'status-dispatched';
|
|
default: return 'status-loading';
|
|
}
|
|
}
|
|
|
|
/* ---------- Init ---------- */
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
bindUI();
|
|
loadDashboard();
|
|
loadAvailableOrders();
|
|
});
|
|
|
|
/* ---------- UI binding ---------- */
|
|
function bindUI(){
|
|
document.getElementById('toggleCreateBtn').addEventListener('click', toggleCreateCard);
|
|
document.getElementById('closeCreateInline').addEventListener('click', () => closeCreateCard());
|
|
document.getElementById('cancelCreateInline').addEventListener('click', () => closeCreateCard());
|
|
document.getElementById('createOrderInlineForm').addEventListener('submit', submitCreateOrderInline);
|
|
document.getElementById('refreshBtn').addEventListener('click', () => { loadDashboard(); loadAvailableOrders(); });
|
|
document.getElementById('toggleConsolidatedBtn').addEventListener('click', toggleConsolidateVisibility);
|
|
document.getElementById('searchBtn').addEventListener('click', handleSearch);
|
|
document.getElementById('addInstallmentFromDetails').addEventListener('click', () => {
|
|
if(!currentEntry) return;
|
|
openInstallmentModal(currentEntry.entry_no, currentEntry.description, currentEntry.region, currentEntry.pending_amount);
|
|
closeEntryDetailsModal();
|
|
});
|
|
|
|
// Installment form submit
|
|
document.getElementById('installmentForm').addEventListener('submit', submitInstallment);
|
|
}
|
|
|
|
/* ---------- Toggle create inline ---------------- */
|
|
function toggleCreateCard(){
|
|
const card = document.getElementById('createCard');
|
|
const btn = document.getElementById('toggleCreateBtn');
|
|
if(card.classList.contains('open')){
|
|
closeCreateCard();
|
|
} else {
|
|
openCreateCard();
|
|
}
|
|
}
|
|
function openCreateCard(){
|
|
const card = document.getElementById('createCard');
|
|
card.classList.add('open');
|
|
document.getElementById('toggleCreateBtn').setAttribute('aria-expanded','true');
|
|
// refresh orders
|
|
loadAvailableOrders();
|
|
// focus first input
|
|
setTimeout(()=> document.getElementById('inline_description').focus(), 220);
|
|
}
|
|
function closeCreateCard(){
|
|
const card = document.getElementById('createCard');
|
|
card.classList.remove('open');
|
|
document.getElementById('toggleCreateBtn').setAttribute('aria-expanded','false');
|
|
}
|
|
|
|
/* ---------- Loaders ---------- */
|
|
function loadDashboard(){
|
|
// show loading placeholders
|
|
document.getElementById('paymentTableBody').innerHTML = '<tr><td colspan="8" class="empty-state">Loading entries...</td></tr>';
|
|
document.getElementById('orderTableBody').innerHTML = '<tr><td colspan="8" class="empty-state">Loading entries...</td></tr>';
|
|
jsonFetch('/admin/account/dashboard')
|
|
.then(res => {
|
|
if(!res.success) throw new Error(res.message || 'Failed to load dashboard');
|
|
entries = res.entries || [];
|
|
renderPaymentTable(entries);
|
|
renderOrderTable(entries);
|
|
document.getElementById('entriesCount').textContent = entries.length;
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
document.getElementById('paymentTableBody').innerHTML = '<tr><td colspan="8" class="empty-state">Unable to load entries</td></tr>';
|
|
document.getElementById('orderTableBody').innerHTML = '<tr><td colspan="8" class="empty-state">Unable to load entries</td></tr>';
|
|
});
|
|
}
|
|
|
|
function loadAvailableOrders(){
|
|
const tbody = document.getElementById('consolidateOrdersBody');
|
|
tbody.innerHTML = '<tr><td colspan="10" class="empty-state">Loading available orders...</td></tr>';
|
|
jsonFetch('/admin/account/available-orders')
|
|
.then(res => {
|
|
if(!res.success) throw new Error(res.message || 'Failed to load orders');
|
|
availableOrders = res.orders || [];
|
|
renderConsolidateOrders(availableOrders);
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
tbody.innerHTML = '<tr><td colspan="10" class="empty-state">Unable to load orders</td></tr>';
|
|
});
|
|
}
|
|
|
|
/* ---------- Renderers ---------- */
|
|
function renderPaymentTable(list){
|
|
const body = document.getElementById('paymentTableBody');
|
|
body.innerHTML = '';
|
|
if(!list || list.length === 0){
|
|
body.innerHTML = '<tr><td colspan="8" class="empty-state">No entries found</td></tr>';
|
|
return;
|
|
}
|
|
list.forEach(entry => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${escapeHtml(entry.entry_no)}</td>
|
|
<td>${escapeHtml(entry.entry_date)}</td>
|
|
<td>${escapeHtml(entry.description)}</td>
|
|
<td>${entry.order_quantity ?? '-'}</td>
|
|
<td>${escapeHtml(entry.region)}</td>
|
|
<td>
|
|
<button class="toggle-switch-btn" data-entry="${escapeHtml(entry.entry_no)}" data-pos="${Number(entry.toggle_pos) || 0}" aria-label="Toggle payment state"></button>
|
|
</td>
|
|
<td>${formatCurrency(entry.amount)}</td>
|
|
<td><span class="status-badge ${statusClass(entry.payment_status)}">${capitalize(entry.payment_status)}</span></td>
|
|
`;
|
|
body.appendChild(tr);
|
|
|
|
const btn = tr.querySelector('.toggle-switch-btn');
|
|
setToggleVisual(btn, Number(entry.toggle_pos));
|
|
btn.addEventListener('click', () => cycleToggle(btn));
|
|
});
|
|
}
|
|
|
|
function renderOrderTable(list){
|
|
const body = document.getElementById('orderTableBody');
|
|
body.innerHTML = '';
|
|
if(!list || list.length === 0){
|
|
body.innerHTML = '<tr><td colspan="8" class="empty-state">No entries found</td></tr>';
|
|
return;
|
|
}
|
|
list.forEach(entry => {
|
|
const tr = document.createElement('tr');
|
|
const pending = Number(entry.pending_amount || 0);
|
|
const pendingHtml = pending <= 0 ? '<span class="status-badge pending-badge-green">Completed</span>' : `<span class="status-badge pending-badge-red">${formatCurrency(pending)}</span>`;
|
|
tr.innerHTML = `
|
|
<td><a class="entry-link" data-entry="${escapeHtml(entry.entry_no)}">${escapeHtml(entry.entry_no)}</a></td>
|
|
<td>${escapeHtml(entry.entry_date)}</td>
|
|
<td>${escapeHtml(entry.description)}</td>
|
|
<td>${escapeHtml(entry.region)}</td>
|
|
<td>${formatCurrency(entry.amount)}</td>
|
|
<td><span class="status-badge ${statusClass(entry.dispatch_status)}">${capitalize(entry.dispatch_status)}</span></td>
|
|
<td>${pendingHtml}</td>
|
|
<td>${pending > 0 ? `<button class="plus-btn" data-entry="${escapeHtml(entry.entry_no)}" title="Add installment">+</button>` : ''}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
|
|
// bind entry link
|
|
const link = tr.querySelector('.entry-link');
|
|
link.addEventListener('click', (e) => openEntryDetailsModal(e.target.dataset.entry));
|
|
|
|
// bind plus
|
|
const plus = tr.querySelector('.plus-btn');
|
|
if(plus) plus.addEventListener('click', (e) => openInstallmentModal(e.target.dataset.entry, entry.description, entry.region, entry.pending_amount));
|
|
});
|
|
}
|
|
|
|
function renderConsolidateOrders(list){
|
|
const body = document.getElementById('consolidateOrdersBody');
|
|
body.innerHTML = '';
|
|
if(!list || list.length === 0){
|
|
body.innerHTML = '<tr><td colspan="10" class="empty-state">No available orders</td></tr>';
|
|
return;
|
|
}
|
|
list.forEach(order => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td><input type="checkbox" value="${order.id}"></td>
|
|
<td><a href="#" class="order-link" data-id="${order.id}">${escapeHtml(order.order_id || ('#'+order.id))}</a></td>
|
|
<td>${escapeHtml(order.mark_no || '')}</td>
|
|
<td>${order.origin ?? ''}</td>
|
|
<td>${order.destination ?? ''}</td>
|
|
<td>${order.ctn ?? ''}</td>
|
|
<td>${order.qty ?? ''}</td>
|
|
<td>${order.ttl_qty ?? ''}</td>
|
|
<td>${order.ttl_amount ?? ''}</td>
|
|
<td>${order.cbm ?? ''}</td>
|
|
<td>${order.ttl_cbm ?? ''}</td>
|
|
<td>${order.kg ?? ''}</td>
|
|
<td>${order.ttl_kg ?? ''}</td>
|
|
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
|
|
function setToggleVisual(btn, pos) {
|
|
btn.classList.remove('mid', 'checked');
|
|
|
|
// 0 = Red (default)
|
|
if (pos === 1) {
|
|
btn.classList.add('mid'); // Yellow
|
|
}
|
|
else if (pos === 2) {
|
|
btn.classList.add('checked'); // Green
|
|
}
|
|
|
|
btn.dataset.pos = pos;
|
|
}
|
|
|
|
function cycleToggle(btn) {
|
|
let pos = Number(btn.dataset.pos) || 0;
|
|
|
|
pos = (pos + 1) % 3; // 0 → 1 → 2 → 0
|
|
|
|
// Live update toggle
|
|
setToggleVisual(btn, pos);
|
|
|
|
// Save to backend
|
|
jsonFetch('/admin/account/toggle-payment', {
|
|
method: 'POST',
|
|
body: {
|
|
entry_no: btn.dataset.entry,
|
|
toggle_pos: pos
|
|
}
|
|
})
|
|
.then(res => {
|
|
if (!res.success) alert(res.message || 'Failed to update');
|
|
loadDashboard(); // sync data
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
loadDashboard();
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/* ---------- Create Order Inline (Option D) ---------- */
|
|
async function submitCreateOrderInline(e){
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const selected = Array.from(document.querySelectorAll('#consolidateOrdersBody input[type=checkbox]:checked')).map(i=>Number(i.value));
|
|
const payload = {
|
|
description: form.description.value.trim(),
|
|
region: form.region.value,
|
|
amount: Number(form.amount.value) || 0,
|
|
entry_date: form.entry_date.value,
|
|
selected_orders: selected
|
|
};
|
|
|
|
if(!payload.description || payload.amount <= 0){
|
|
alert('Please enter a valid description and amount.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Creating...';
|
|
const res = await jsonFetch('/admin/account/create-order', { method:'POST', body: payload });
|
|
if(!res.success) throw new Error(res.message || 'Create failed');
|
|
// success
|
|
form.reset();
|
|
closeCreateCard();
|
|
loadDashboard();
|
|
loadAvailableOrders();
|
|
} catch(err){
|
|
alert(err.message || 'Failed to create order');
|
|
console.error(err);
|
|
} finally {
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
if(btn){ btn.disabled = false; btn.textContent = 'Create Order'; }
|
|
}
|
|
}
|
|
|
|
/* ---------- Consolidate toggle ---------- */
|
|
function toggleConsolidateVisibility(){
|
|
const btn = document.getElementById('toggleConsolidatedBtn');
|
|
const area = document.getElementById('consolidateArea');
|
|
if(btn.classList.contains('active')){
|
|
btn.classList.remove('active'); btn.textContent = 'Show Orders';
|
|
area.style.display = 'none';
|
|
} else {
|
|
btn.classList.add('active'); btn.textContent = 'Hide Orders';
|
|
area.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/* ---------- Search ---------- */
|
|
function handleSearch(){
|
|
const q = document.getElementById('main-search').value.trim().toLowerCase();
|
|
if(!q){ renderPaymentTable(entries); renderOrderTable(entries); return; }
|
|
const filtered = entries.filter(e => {
|
|
return String(e.entry_no || '').toLowerCase().includes(q) ||
|
|
String(e.description || '').toLowerCase().includes(q) ||
|
|
String(e.region || '').toLowerCase().includes(q);
|
|
});
|
|
renderPaymentTable(filtered);
|
|
renderOrderTable(filtered);
|
|
}
|
|
|
|
/* ---------- Entry details & installments ---------- */
|
|
async function openEntryDetailsModal(entryNo) {
|
|
try {
|
|
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
|
|
|
|
if (!res.success) throw new Error(res.message || 'Failed to load entry');
|
|
|
|
const entry = res.entry;
|
|
currentEntry = entry;
|
|
|
|
document.getElementById('entryDetailsId').textContent = entry.entry_no;
|
|
document.getElementById('originalAmount').textContent = formatCurrency(entry.amount);
|
|
|
|
const totalProcessed = Number(entry.amount) - Number(entry.pending_amount);
|
|
document.getElementById('totalProcessed').textContent = formatCurrency(totalProcessed);
|
|
|
|
document.getElementById('pendingBalance').textContent = formatCurrency(entry.pending_amount);
|
|
document.getElementById('totalInstallments').textContent = entry.installments.length;
|
|
|
|
const tbody = document.getElementById('installmentsTableBody');
|
|
tbody.innerHTML = '';
|
|
|
|
entry.installments.forEach((ins, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${idx === 0 ? 'Original Entry' : 'Installment ' + idx}</td>
|
|
<td>${escapeHtml(ins.proc_date)}</td>
|
|
<td>${escapeHtml(ins.description)}</td>
|
|
<td>${escapeHtml(ins.region)}</td>
|
|
<td>${formatCurrency(ins.amount)}</td>
|
|
|
|
<td>
|
|
<select class="installment-status-dropdown"
|
|
data-id="${ins.id}"
|
|
onchange="updateInstallmentStatus(${ins.id}, this.value)">
|
|
<option value="Pending" ${ins.status === 'Pending' ? 'selected' : ''}>Pending</option>
|
|
<option value="Loading" ${ins.status === 'Loading' ? 'selected' : ''}>Loading</option>
|
|
<option value="Packed" ${ins.status === 'Packed' ? 'selected' : ''}>Packed</option>
|
|
<option value="Dispatched" ${ins.status === 'Dispatched' ? 'selected' : ''}>Dispatched</option>
|
|
<option value="Delivered" ${ins.status === 'Delivered' ? 'selected' : ''}>Delivered</option>
|
|
</select>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
document.getElementById('entryDetailsModal').classList.add('modal-open');
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Unable to load entry details');
|
|
}
|
|
}
|
|
|
|
function closeEntryDetailsModal() {
|
|
document.getElementById('entryDetailsModal').classList.remove('modal-open');
|
|
}
|
|
|
|
|
|
|
|
async function updateInstallmentStatus(id, status) {
|
|
try {
|
|
const res = await jsonFetch('/admin/account/installment/update-status', {
|
|
method: 'POST',
|
|
body: { installment_id: id, status: status }
|
|
});
|
|
|
|
if (!res.success) {
|
|
alert('Failed to update status');
|
|
return;
|
|
}
|
|
|
|
// Refresh details modal instantly
|
|
openEntryDetailsModal(res.entry.entry_no);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Unable to update installment status');
|
|
}
|
|
}
|
|
|
|
|
|
/* ---------- Installment modal ---------- */
|
|
function openInstallmentModal(entryNo, desc, region, pending){
|
|
currentEntry = { entry_no: entryNo, description: desc, region: region, pending_amount: pending };
|
|
document.getElementById('instDetailsRow').innerHTML = `
|
|
<div style="min-width:120px"><div class="kv">Entry No</div><div>${escapeHtml(entryNo)}</div></div>
|
|
<div style="min-width:160px"><div class="kv">Description</div><div>${escapeHtml(desc)}</div></div>
|
|
<div style="min-width:120px"><div class="kv">Region</div><div>${escapeHtml(region)}</div></div>
|
|
<div style="min-width:120px"><div class="kv">Pending</div><div>${formatCurrency(pending)}</div></div>
|
|
`;
|
|
document.getElementById('maxPending').textContent = formatCurrency(pending);
|
|
document.getElementById('installmentModal').classList.add('modal-open');
|
|
|
|
// set example default value to pending (but not exceed)
|
|
const amountInput = document.querySelector('#installmentForm input[name="proc_amount"]');
|
|
if(amountInput) amountInput.value = pending ? Number(pending) : '';
|
|
}
|
|
|
|
function closeInstallmentModal(){ document.getElementById('installmentModal').classList.remove('modal-open'); }
|
|
|
|
async function submitInstallment(e){
|
|
e.preventDefault();
|
|
if(!currentEntry){ alert('No entry selected'); return; }
|
|
const form = e.target;
|
|
const payload = { entry_no: currentEntry.entry_no, proc_date: form.proc_date.value, amount: Number(form.proc_amount.value) || 0, status: form.status.value };
|
|
|
|
if(payload.amount > Number(currentEntry.pending_amount || 0)){
|
|
alert('Amount cannot exceed pending amount'); return;
|
|
}
|
|
try {
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
btn.disabled = true; btn.textContent = 'Submitting...';
|
|
const res = await jsonFetch('/admin/account/installment/create', { method:'POST', body: payload });
|
|
if(!res.success) throw new Error(res.message || 'Failed to create installment');
|
|
closeInstallmentModal();
|
|
await loadDashboard();
|
|
openEntryDetailsModal(currentEntry.entry_no); // open updated details
|
|
} catch(err){
|
|
console.error(err);
|
|
alert(err.message || 'Failed to create installment');
|
|
} finally {
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
if(btn){ btn.disabled = false; btn.textContent = 'Create Installment'; }
|
|
}
|
|
}
|
|
|
|
/* ---------- Utilities ---------- */
|
|
// ensure consolidate area visible by default
|
|
document.addEventListener('DOMContentLoaded', () => { document.getElementById('consolidateArea').style.display = 'block'; });
|
|
</script>
|
|
|
|
@endsection
|