| 37750 |
ranu |
1 |
## PO Stock Approvals โ queue view for approvers.
|
|
|
2 |
## Route: /po/stock-approvals. Detail modal fetched from /po/stock-approvals/{poId}.
|
|
|
3 |
## Row-click opens the modal; approve/reject fires from inside via pso:decided event.
|
|
|
4 |
<style type="text/css">
|
|
|
5 |
.psov2 {
|
|
|
6 |
--brand: #3b5bdb;
|
|
|
7 |
--brand2: #28449e;
|
|
|
8 |
--ink: #1a2027;
|
|
|
9 |
--soft: #6b7280;
|
|
|
10 |
--line: #e5e7eb;
|
|
|
11 |
--line2: #f0f2f6;
|
|
|
12 |
--bg: #f7f8fb;
|
|
|
13 |
--good: #16a34a;
|
|
|
14 |
--good-bg: #dcfce7;
|
|
|
15 |
--warn: #ea580c;
|
|
|
16 |
--warn-bg: #ffedd5;
|
|
|
17 |
--bad: #dc2626;
|
|
|
18 |
--bad-bg: #fee2e2;
|
|
|
19 |
--gray-bg: #f1f5f9;
|
|
|
20 |
background: var(--bg);
|
|
|
21 |
padding: 20px 22px 60px;
|
|
|
22 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
|
23 |
color: var(--ink);
|
|
|
24 |
min-height: 100vh
|
|
|
25 |
}
|
|
|
26 |
|
|
|
27 |
.psov2 .hdr {
|
|
|
28 |
display: flex;
|
|
|
29 |
justify-content: space-between;
|
|
|
30 |
align-items: flex-end;
|
|
|
31 |
margin-bottom: 22px
|
|
|
32 |
}
|
|
|
33 |
|
|
|
34 |
.psov2 .hdr h1 {
|
|
|
35 |
font-size: 26px;
|
|
|
36 |
font-weight: 800;
|
|
|
37 |
margin: 0;
|
|
|
38 |
letter-spacing: -.01em
|
|
|
39 |
}
|
|
|
40 |
|
|
|
41 |
.psov2 .hdr .sub {
|
|
|
42 |
color: var(--soft);
|
|
|
43 |
font-size: 13.5px;
|
|
|
44 |
margin-top: 4px
|
|
|
45 |
}
|
|
|
46 |
|
|
|
47 |
.psov2 .hdr .live {
|
|
|
48 |
display: inline-flex;
|
|
|
49 |
align-items: center;
|
|
|
50 |
gap: 6px;
|
|
|
51 |
font-size: 11px;
|
|
|
52 |
color: var(--good);
|
|
|
53 |
background: var(--good-bg);
|
|
|
54 |
border-radius: 20px;
|
|
|
55 |
padding: 4px 10px;
|
|
|
56 |
font-weight: 700;
|
|
|
57 |
letter-spacing: .05em;
|
|
|
58 |
text-transform: uppercase;
|
|
|
59 |
margin-left: 8px
|
|
|
60 |
}
|
|
|
61 |
|
|
|
62 |
.psov2 .hdr .live::before {
|
|
|
63 |
content: "";
|
|
|
64 |
width: 6px;
|
|
|
65 |
height: 6px;
|
|
|
66 |
border-radius: 50%;
|
|
|
67 |
background: var(--good)
|
|
|
68 |
}
|
|
|
69 |
|
|
|
70 |
/* --- KPI cards --- */
|
|
|
71 |
.psov2 .kpis {
|
|
|
72 |
display: grid;
|
|
|
73 |
grid-template-columns: repeat(5, 1fr);
|
|
|
74 |
gap: 14px;
|
|
|
75 |
margin-bottom: 22px
|
|
|
76 |
}
|
|
|
77 |
|
|
|
78 |
.psov2 .kpi {
|
|
|
79 |
background: #fff;
|
|
|
80 |
border-radius: 12px;
|
|
|
81 |
padding: 16px 18px;
|
|
|
82 |
display: flex;
|
|
|
83 |
align-items: center;
|
|
|
84 |
gap: 14px;
|
|
|
85 |
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
|
|
86 |
border: 1px solid var(--line)
|
|
|
87 |
}
|
|
|
88 |
|
|
|
89 |
.psov2 .kpi .icn {
|
|
|
90 |
width: 44px;
|
|
|
91 |
height: 44px;
|
|
|
92 |
border-radius: 10px;
|
|
|
93 |
display: flex;
|
|
|
94 |
align-items: center;
|
|
|
95 |
justify-content: center;
|
|
|
96 |
font-size: 20px;
|
|
|
97 |
flex-shrink: 0
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
.psov2 .kpi .txt .num {
|
|
|
101 |
font-size: 22px;
|
|
|
102 |
font-weight: 800;
|
|
|
103 |
line-height: 1.1;
|
|
|
104 |
font-variant-numeric: tabular-nums
|
|
|
105 |
}
|
|
|
106 |
|
|
|
107 |
.psov2 .kpi .txt .lbl {
|
|
|
108 |
font-size: 11.5px;
|
|
|
109 |
color: var(--soft);
|
|
|
110 |
font-weight: 600;
|
|
|
111 |
margin-top: 2px
|
|
|
112 |
}
|
|
|
113 |
|
|
|
114 |
.psov2 .kpi.i-pending .icn {
|
|
|
115 |
background: #eef2fb;
|
|
|
116 |
color: var(--brand)
|
|
|
117 |
}
|
|
|
118 |
|
|
|
119 |
.psov2 .kpi.i-stale .icn {
|
|
|
120 |
background: var(--warn-bg);
|
|
|
121 |
color: var(--warn)
|
|
|
122 |
}
|
|
|
123 |
|
|
|
124 |
.psov2 .kpi.i-approved .icn {
|
|
|
125 |
background: var(--good-bg);
|
|
|
126 |
color: var(--good)
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
.psov2 .kpi.i-rejected .icn {
|
|
|
130 |
background: var(--bad-bg);
|
|
|
131 |
color: var(--bad)
|
|
|
132 |
}
|
|
|
133 |
|
|
|
134 |
.psov2 .kpi.i-excess .icn {
|
|
|
135 |
background: #f3e8ff;
|
|
|
136 |
color: #7c3aed
|
|
|
137 |
}
|
|
|
138 |
|
|
|
139 |
/* --- Main card --- */
|
|
|
140 |
.psov2 .card {
|
|
|
141 |
background: #fff;
|
|
|
142 |
border-radius: 12px;
|
|
|
143 |
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
|
|
144 |
border: 1px solid var(--line);
|
|
|
145 |
overflow: hidden
|
|
|
146 |
}
|
|
|
147 |
|
|
|
148 |
.psov2 .card-hd {
|
|
|
149 |
display: flex;
|
|
|
150 |
justify-content: space-between;
|
|
|
151 |
align-items: center;
|
|
|
152 |
padding: 16px 22px;
|
|
|
153 |
border-bottom: 1px solid var(--line)
|
|
|
154 |
}
|
|
|
155 |
|
|
|
156 |
.psov2 .card-hd .title h2 {
|
|
|
157 |
margin: 0;
|
|
|
158 |
font-size: 17px;
|
|
|
159 |
font-weight: 700
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
.psov2 .card-hd .title .sub {
|
|
|
163 |
color: var(--soft);
|
|
|
164 |
font-size: 12.5px;
|
|
|
165 |
margin-top: 2px
|
|
|
166 |
}
|
|
|
167 |
|
|
|
168 |
/* --- Filter tabs --- */
|
|
|
169 |
.psov2 .tabs {
|
|
|
170 |
display: inline-flex;
|
|
|
171 |
background: var(--gray-bg);
|
|
|
172 |
border-radius: 8px;
|
|
|
173 |
padding: 3px
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
.psov2 .tabs .tab {
|
|
|
177 |
padding: 6px 14px;
|
|
|
178 |
font-size: 12.5px;
|
|
|
179 |
font-weight: 650;
|
|
|
180 |
color: var(--soft);
|
|
|
181 |
background: transparent;
|
|
|
182 |
border: none;
|
|
|
183 |
border-radius: 6px;
|
|
|
184 |
cursor: pointer;
|
|
|
185 |
transition: background .1s, color .1s
|
|
|
186 |
}
|
|
|
187 |
|
|
|
188 |
.psov2 .tabs .tab.on {
|
|
|
189 |
background: var(--good);
|
|
|
190 |
color: #fff;
|
|
|
191 |
box-shadow: 0 1px 2px rgba(22, 163, 74, .18)
|
|
|
192 |
}
|
|
|
193 |
|
|
|
194 |
.psov2 .tabs .tab.on.pending {
|
|
|
195 |
background: var(--brand);
|
|
|
196 |
box-shadow: 0 1px 2px rgba(59, 91, 219, .2)
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
.psov2 .tabs .tab.on.rejected {
|
|
|
200 |
background: var(--bad);
|
|
|
201 |
box-shadow: 0 1px 2px rgba(220, 38, 38, .2)
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
/* --- List row --- Uses .psov2-list / .psov2-row (NOT Bootstrap's .row which
|
|
|
205 |
applies conflicting flex + negative margins from the parent admin frame). */
|
|
|
206 |
.psov2 .psov2-list {
|
|
|
207 |
padding: 4px 0
|
|
|
208 |
}
|
|
|
209 |
|
|
|
210 |
.psov2 .psov2-row {
|
|
|
211 |
display: grid;
|
|
|
212 |
grid-template-columns: minmax(200px, 2fr) minmax(160px, 1.6fr) minmax(140px, 1.2fr) minmax(180px, 2fr) 110px 40px;
|
|
|
213 |
column-gap: 20px;
|
|
|
214 |
row-gap: 0;
|
|
|
215 |
align-items: center;
|
|
|
216 |
padding: 14px 22px;
|
|
|
217 |
margin: 0;
|
|
|
218 |
border-bottom: 1px solid var(--line2);
|
|
|
219 |
cursor: pointer;
|
|
|
220 |
transition: background .08s;
|
|
|
221 |
background: #fff
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
/* Hidden state must beat display:grid โ used by tab filter. */
|
|
|
225 |
.psov2 .psov2-row.hide {
|
|
|
226 |
display: none
|
|
|
227 |
}
|
|
|
228 |
|
|
|
229 |
.psov2 .psov2-row:last-child {
|
|
|
230 |
border-bottom: none
|
|
|
231 |
}
|
|
|
232 |
|
|
|
233 |
.psov2 .psov2-row:hover {
|
|
|
234 |
background: #fafbfe
|
|
|
235 |
}
|
|
|
236 |
|
|
|
237 |
.psov2 .psov2-row > * {
|
|
|
238 |
min-width: 0 /* let grid columns actually shrink instead of overflowing */
|
|
|
239 |
}
|
|
|
240 |
|
|
|
241 |
.psov2 .r-primary .pn {
|
|
|
242 |
font-size: 14px;
|
|
|
243 |
font-weight: 700;
|
|
|
244 |
color: var(--brand);
|
|
|
245 |
font-variant-numeric: tabular-nums;
|
|
|
246 |
white-space: nowrap;
|
|
|
247 |
overflow: hidden;
|
|
|
248 |
text-overflow: ellipsis
|
|
|
249 |
}
|
|
|
250 |
|
|
|
251 |
.psov2 .r-primary .rec {
|
|
|
252 |
font-size: 11.5px;
|
|
|
253 |
color: var(--soft);
|
|
|
254 |
margin-top: 3px;
|
|
|
255 |
white-space: nowrap;
|
|
|
256 |
overflow: hidden;
|
|
|
257 |
text-overflow: ellipsis
|
|
|
258 |
}
|
|
|
259 |
|
|
|
260 |
.psov2 .r-vendor .v {
|
|
|
261 |
font-size: 13px;
|
|
|
262 |
color: var(--ink);
|
|
|
263 |
font-weight: 600;
|
|
|
264 |
white-space: nowrap;
|
|
|
265 |
overflow: hidden;
|
|
|
266 |
text-overflow: ellipsis
|
|
|
267 |
}
|
|
|
268 |
|
|
|
269 |
.psov2 .r-vendor .w {
|
|
|
270 |
font-size: 11.5px;
|
|
|
271 |
color: var(--soft);
|
|
|
272 |
margin-top: 3px;
|
|
|
273 |
white-space: nowrap;
|
|
|
274 |
overflow: hidden;
|
|
|
275 |
text-overflow: ellipsis;
|
|
|
276 |
font-weight: 600;
|
|
|
277 |
letter-spacing: .04em;
|
|
|
278 |
text-transform: uppercase
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
.psov2 .r-flags .fc {
|
|
|
282 |
font-size: 13px;
|
|
|
283 |
font-weight: 700;
|
|
|
284 |
color: var(--ink)
|
|
|
285 |
}
|
|
|
286 |
|
|
|
287 |
.psov2 .r-flags .fx {
|
|
|
288 |
font-size: 12px;
|
|
|
289 |
color: var(--bad);
|
|
|
290 |
font-weight: 600;
|
|
|
291 |
margin-top: 2px
|
|
|
292 |
}
|
|
|
293 |
|
|
|
294 |
.psov2 .r-reason {
|
|
|
295 |
font-size: 12.5px;
|
|
|
296 |
color: var(--ink);
|
|
|
297 |
line-height: 1.4;
|
|
|
298 |
display: -webkit-box;
|
|
|
299 |
-webkit-line-clamp: 2;
|
|
|
300 |
-webkit-box-orient: vertical;
|
|
|
301 |
overflow: hidden
|
|
|
302 |
}
|
|
|
303 |
|
|
|
304 |
.psov2 .r-reason.empty {
|
|
|
305 |
color: var(--soft);
|
|
|
306 |
font-style: italic
|
|
|
307 |
}
|
|
|
308 |
|
|
|
309 |
.psov2 .badge {
|
|
|
310 |
display: inline-flex;
|
|
|
311 |
align-items: center;
|
|
|
312 |
padding: 5px 12px;
|
|
|
313 |
border-radius: 20px;
|
|
|
314 |
font-size: 11px;
|
|
|
315 |
font-weight: 700;
|
|
|
316 |
letter-spacing: .05em;
|
|
|
317 |
text-transform: uppercase
|
|
|
318 |
}
|
|
|
319 |
|
|
|
320 |
.psov2 .badge.PENDING {
|
|
|
321 |
background: var(--warn-bg);
|
|
|
322 |
color: var(--warn)
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
.psov2 .badge.APPROVED {
|
|
|
326 |
background: var(--good-bg);
|
|
|
327 |
color: var(--good)
|
|
|
328 |
}
|
|
|
329 |
|
|
|
330 |
.psov2 .badge.REJECTED {
|
|
|
331 |
background: var(--bad-bg);
|
|
|
332 |
color: var(--bad)
|
|
|
333 |
}
|
|
|
334 |
|
|
|
335 |
.psov2 .r-badge {
|
|
|
336 |
text-align: center
|
|
|
337 |
}
|
|
|
338 |
|
|
|
339 |
.psov2 .r-actions {
|
|
|
340 |
display: flex;
|
|
|
341 |
justify-content: flex-end;
|
|
|
342 |
align-items: center
|
|
|
343 |
}
|
|
|
344 |
|
|
|
345 |
.psov2 .icon-btn {
|
|
|
346 |
width: 34px;
|
|
|
347 |
height: 34px;
|
|
|
348 |
padding: 0;
|
|
|
349 |
display: inline-flex;
|
|
|
350 |
align-items: center;
|
|
|
351 |
justify-content: center;
|
|
|
352 |
background: var(--good-bg);
|
|
|
353 |
color: var(--good);
|
|
|
354 |
border: 1px solid transparent;
|
|
|
355 |
border-radius: 8px;
|
|
|
356 |
font-size: 14px;
|
|
|
357 |
cursor: pointer;
|
|
|
358 |
transition: background .1s, transform .05s
|
|
|
359 |
}
|
|
|
360 |
|
|
|
361 |
.psov2 .icon-btn:active {
|
|
|
362 |
transform: scale(.94)
|
|
|
363 |
}
|
|
|
364 |
|
|
|
365 |
.psov2 .icon-btn:hover {
|
|
|
366 |
background: #bbf7d0
|
|
|
367 |
}
|
|
|
368 |
|
|
|
369 |
.psov2 .icon-btn.psov2-view {
|
|
|
370 |
background: var(--good-bg);
|
|
|
371 |
color: var(--good)
|
|
|
372 |
}
|
|
|
373 |
|
|
|
374 |
.psov2 .empty {
|
|
|
375 |
text-align: center;
|
|
|
376 |
padding: 44px 20px;
|
|
|
377 |
color: var(--soft)
|
|
|
378 |
}
|
|
|
379 |
|
|
|
380 |
.psov2 .empty .ei {
|
|
|
381 |
font-size: 34px;
|
|
|
382 |
margin-bottom: 10px
|
|
|
383 |
}
|
|
|
384 |
|
|
|
385 |
.psov2 .empty b {
|
|
|
386 |
color: var(--ink);
|
|
|
387 |
font-size: 14px
|
|
|
388 |
}
|
|
|
389 |
|
|
|
390 |
.psov2 .rule-note {
|
|
|
391 |
color: var(--soft);
|
|
|
392 |
font-size: 12px;
|
|
|
393 |
padding: 10px 22px;
|
|
|
394 |
border-top: 1px solid var(--line);
|
|
|
395 |
background: #fafbfe
|
|
|
396 |
}
|
|
|
397 |
|
|
|
398 |
.psov2 .rule-note code {
|
|
|
399 |
background: #eef2fb;
|
|
|
400 |
color: var(--brand);
|
|
|
401 |
padding: 1px 5px;
|
|
|
402 |
border-radius: 3px;
|
|
|
403 |
font-size: 11px
|
|
|
404 |
}
|
|
|
405 |
|
|
|
406 |
@media (max-width: 1100px) {
|
|
|
407 |
.psov2 .kpis {
|
|
|
408 |
grid-template-columns: repeat(3, 1fr)
|
|
|
409 |
}
|
|
|
410 |
|
|
|
411 |
.psov2 .psov2-row {
|
|
|
412 |
grid-template-columns: minmax(180px, 2fr) minmax(140px, 1.4fr) 110px 40px;
|
|
|
413 |
row-gap: 6px
|
|
|
414 |
}
|
|
|
415 |
|
|
|
416 |
.psov2 .r-flags, .psov2 .r-reason {
|
|
|
417 |
display: none
|
|
|
418 |
}
|
|
|
419 |
}
|
|
|
420 |
</style>
|
|
|
421 |
|
|
|
422 |
<div class="psov2">
|
|
|
423 |
<div class="hdr">
|
|
|
424 |
<div>
|
|
|
425 |
<h1>PO Stock Approvals <span class="live">Live</span></h1>
|
|
|
426 |
<div class="sub">Purchase orders where <b>aging stock + PO qty</b> exceeds last-15-day sales.</div>
|
|
|
427 |
</div>
|
|
|
428 |
</div>
|
|
|
429 |
|
|
|
430 |
<!-- KPI tiles -->
|
|
|
431 |
<div class="kpis">
|
|
|
432 |
<div class="kpi i-pending">
|
|
|
433 |
<div class="icn">๐</div>
|
|
|
434 |
<div class="txt">
|
|
|
435 |
<div class="num">$!pendingCount</div>
|
|
|
436 |
<div class="lbl">Pending</div>
|
|
|
437 |
</div>
|
|
|
438 |
</div>
|
|
|
439 |
<div class="kpi i-stale">
|
|
|
440 |
<div class="icn">โฐ</div>
|
|
|
441 |
<div class="txt">
|
|
|
442 |
<div class="num">$!staleCount</div>
|
|
|
443 |
<div class="lbl">Pending > 24h</div>
|
|
|
444 |
</div>
|
|
|
445 |
</div>
|
|
|
446 |
<div class="kpi i-approved">
|
|
|
447 |
<div class="icn">โ</div>
|
|
|
448 |
<div class="txt">
|
|
|
449 |
<div class="num">$!approvedTodayCount</div>
|
|
|
450 |
<div class="lbl">Approved today</div>
|
|
|
451 |
</div>
|
|
|
452 |
</div>
|
|
|
453 |
<div class="kpi i-rejected">
|
|
|
454 |
<div class="icn">โ</div>
|
|
|
455 |
<div class="txt">
|
|
|
456 |
<div class="num">$!rejectedTodayCount</div>
|
|
|
457 |
<div class="lbl">Rejected today</div>
|
|
|
458 |
</div>
|
|
|
459 |
</div>
|
|
|
460 |
<div class="kpi i-excess">
|
|
|
461 |
<div class="icn">๐ฆ</div>
|
|
|
462 |
<div class="txt">
|
|
|
463 |
<div class="num">+$!totalPendingExcess</div>
|
|
|
464 |
<div class="lbl">Excess in queue</div>
|
|
|
465 |
</div>
|
|
|
466 |
</div>
|
|
|
467 |
</div>
|
|
|
468 |
|
|
|
469 |
<!-- Main list card -->
|
|
|
470 |
<div class="card">
|
|
|
471 |
<div class="card-hd">
|
|
|
472 |
<div class="title">
|
|
|
473 |
<h2>Approvals Queue</h2>
|
|
|
474 |
<div class="sub">Click any row to review the flagged items and decide.</div>
|
|
|
475 |
</div>
|
|
|
476 |
<div class="tabs" id="psov2Tabs">
|
|
|
477 |
<button type="button" class="tab on pending" data-filter="PENDING">Pending</button>
|
|
|
478 |
<button type="button" class="tab" data-filter="APPROVED">Approved</button>
|
|
|
479 |
<button type="button" class="tab" data-filter="REJECTED">Rejected</button>
|
|
|
480 |
<button type="button" class="tab" data-filter="ALL">All</button>
|
|
|
481 |
</div>
|
|
|
482 |
</div>
|
|
|
483 |
|
|
|
484 |
#if($!error)
|
|
|
485 |
<div class="empty">
|
|
|
486 |
<div class="ei">๐</div>
|
|
|
487 |
<b>$error</b></div>
|
|
|
488 |
#else
|
|
|
489 |
<div class="psov2-list" id="psov2Rows">
|
|
|
490 |
#foreach($r in $rows)
|
|
|
491 |
<div class="psov2-row" data-status="$!r.status" data-poid="$!r.poId">
|
|
|
492 |
<div class="r-primary">
|
|
|
493 |
<div class="pn">$!r.poNumber</div>
|
|
|
494 |
<div class="rec">$!r.requesterName ยท $!r.createdAt</div>
|
|
|
495 |
</div>
|
|
|
496 |
<div class="r-vendor">
|
|
|
497 |
<div class="v">$!r.vendorName</div>
|
|
|
498 |
<div class="w">$!r.warehouseName</div>
|
|
|
499 |
</div>
|
|
|
500 |
<div class="r-flags">
|
|
|
501 |
<div class="fc">$!r.flaggedCount item#if($r.flaggedCount != 1)s#end flagged</div>
|
|
|
502 |
<div class="fx">+$!r.totalExcess excess</div>
|
|
|
503 |
</div>
|
|
|
504 |
<div class="r-reason#if($!r.requesterReason == '') empty#end" title="$!r.requesterReason">
|
|
|
505 |
#if($!r.requesterReason == '')No reason on file#else$!r.requesterReason#end
|
|
|
506 |
</div>
|
|
|
507 |
<div class="r-badge">
|
|
|
508 |
<span class="badge $!r.status">$!r.status</span>
|
|
|
509 |
</div>
|
|
|
510 |
<div class="r-actions">
|
|
|
511 |
<button type="button" class="icon-btn psov2-view" title="Review this approval">
|
|
|
512 |
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
513 |
stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
|
|
514 |
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
|
515 |
<circle cx="12" cy="12" r="3"/>
|
|
|
516 |
</svg>
|
|
|
517 |
</button>
|
|
|
518 |
</div>
|
|
|
519 |
</div>
|
|
|
520 |
#end
|
|
|
521 |
</div>
|
|
|
522 |
<div class="empty" id="psov2EmptyState" style="display:none">
|
|
|
523 |
<div class="ei">๐ญ</div>
|
|
|
524 |
<b>No approvals match this filter.</b>
|
|
|
525 |
<div style="margin-top:4px">Try switching tabs above.</div>
|
|
|
526 |
</div>
|
|
|
527 |
#end
|
|
|
528 |
|
|
|
529 |
<div class="rule-note">
|
|
|
530 |
Rule: a PO is held here when <code>agingStock + poQty > last-15-day sales</code>
|
|
|
531 |
for any of its items. Requester must provide a reason before submission.
|
|
|
532 |
</div>
|
|
|
533 |
</div>
|
|
|
534 |
</div>
|
|
|
535 |
|
|
|
536 |
<!-- Detail modal (unchanged from v1) -->
|
|
|
537 |
<div class="modal fade" id="psoDetailModal" tabindex="-1" role="dialog" aria-hidden="true">
|
|
|
538 |
<div class="modal-dialog modal-lg" role="document" style="width:90%;max-width:1100px;margin:30px auto">
|
|
|
539 |
<div class="modal-content">
|
|
|
540 |
<div class="modal-header" style="padding:12px 20px">
|
|
|
541 |
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
|
542 |
<span aria-hidden="true">×</span>
|
|
|
543 |
</button>
|
|
|
544 |
<h4 class="modal-title" style="margin:0">PO Stock Approval</h4>
|
|
|
545 |
</div>
|
|
|
546 |
<div class="modal-body" id="psoDetailBody"
|
|
|
547 |
style="max-height:calc(100vh - 140px);overflow-y:auto;padding:20px">
|
|
|
548 |
<div style="text-align:center;padding:24px;color:#888">Loading…</div>
|
|
|
549 |
</div>
|
|
|
550 |
</div>
|
|
|
551 |
</div>
|
|
|
552 |
</div>
|
|
|
553 |
|
|
|
554 |
<script type="text/javascript">
|
|
|
555 |
(function () {
|
|
|
556 |
var ctx = "${rc.contextPath}";
|
|
|
557 |
var initialFilter = "$!initialFilter";
|
|
|
558 |
|
|
|
559 |
// Uses .hide class (which has display:none) instead of inline style so it
|
|
|
560 |
// beats display:grid without needing !important.
|
|
|
561 |
function applyFilter(f) {
|
|
|
562 |
var visible = 0;
|
|
|
563 |
$("#psov2Rows .psov2-row").each(function () {
|
|
|
564 |
var s = this.getAttribute("data-status");
|
|
|
565 |
var show = (f === "ALL") || (s === f);
|
|
|
566 |
if (show) {
|
|
|
567 |
this.classList.remove("hide");
|
|
|
568 |
visible++;
|
|
|
569 |
} else {
|
|
|
570 |
this.classList.add("hide");
|
|
|
571 |
}
|
|
|
572 |
});
|
|
|
573 |
$("#psov2EmptyState").toggle(visible === 0);
|
|
|
574 |
$("#psov2Tabs .tab").removeClass("on pending rejected");
|
|
|
575 |
var btn = $("#psov2Tabs .tab[data-filter='" + f + "']");
|
|
|
576 |
btn.addClass("on");
|
|
|
577 |
if (f === "PENDING") btn.addClass("pending");
|
|
|
578 |
if (f === "REJECTED") btn.addClass("rejected");
|
|
|
579 |
}
|
|
|
580 |
|
|
|
581 |
applyFilter(initialFilter || "PENDING");
|
|
|
582 |
|
|
|
583 |
// Off/on so reloads don't stack duplicate handlers.
|
|
|
584 |
$("#psov2Tabs .tab").off("click.psov2tab").on("click.psov2tab", function () {
|
|
|
585 |
applyFilter(this.getAttribute("data-filter"));
|
|
|
586 |
});
|
|
|
587 |
|
|
|
588 |
// Row click โ open detail modal. Defensive guard: bail out if the row
|
|
|
589 |
// is missing data-poid (would otherwise send /po/stock-approvals/undefined
|
|
|
590 |
// and produce a NumberFormatException server-side).
|
|
|
591 |
//
|
|
|
592 |
// The nuclear off('click', '.psov2-row') purges ANY previously-bound
|
|
|
593 |
// click handler on this selector โ including handlers from older template
|
|
|
594 |
// versions still lingering in memory after a partial reload. Only THEN do
|
|
|
595 |
// we register the current guarded version.
|
|
|
596 |
$(document).off('click', '.psov2-row');
|
|
|
597 |
$(document).off('click.psov2 click.pso').on('click.psov2', '.psov2-row', function (ev) {
|
|
|
598 |
var poId = this.getAttribute('data-poid');
|
|
|
599 |
if (!poId || poId === 'undefined' || poId === 'null' || isNaN(parseInt(poId, 10))) {
|
|
|
600 |
console.warn('psov2: row missing valid data-poid, ignoring click', this);
|
|
|
601 |
return;
|
|
|
602 |
}
|
|
|
603 |
// Coerce to integer explicitly โ belt-and-braces against string weirdness.
|
|
|
604 |
poId = parseInt(poId, 10);
|
|
|
605 |
$('#psoDetailBody').html('<div style="text-align:center;padding:24px;color:#888">Loading…</div>');
|
|
|
606 |
$('#psoDetailModal').modal('show');
|
|
|
607 |
$.ajax({
|
|
|
608 |
url: ctx + '/po/stock-approvals/' + poId,
|
|
|
609 |
type: 'GET',
|
|
|
610 |
dataType: 'html',
|
|
|
611 |
global: false,
|
|
|
612 |
success: function (html) {
|
|
|
613 |
$('#psoDetailBody').html(html);
|
|
|
614 |
},
|
|
|
615 |
error: function () {
|
|
|
616 |
$('#psoDetailBody').html('<div style="text-align:center;padding:24px;color:#c00">Failed to load detail. Try again.</div>');
|
|
|
617 |
}
|
|
|
618 |
});
|
|
|
619 |
});
|
|
|
620 |
|
|
|
621 |
// After decide โ close modal cleanly + reload the queue.
|
|
|
622 |
$(document).off('pso:decided').on('pso:decided', function () {
|
|
|
623 |
var reloaded = false;
|
|
|
624 |
var doReload = function () {
|
|
|
625 |
if (reloaded) return;
|
|
|
626 |
reloaded = true;
|
|
|
627 |
$('.modal-backdrop').remove();
|
|
|
628 |
$('body').removeClass('modal-open').css('padding-right', '');
|
|
|
629 |
// Full re-render โ KPI counts need re-computing after a decision.
|
|
|
630 |
loadPoStockApprovals('main-content');
|
|
|
631 |
};
|
|
|
632 |
var modalEl = $('#psoDetailModal');
|
|
|
633 |
modalEl.one('hidden.bs.modal', doReload);
|
|
|
634 |
modalEl.modal('hide');
|
|
|
635 |
setTimeout(doReload, 800);
|
|
|
636 |
});
|
|
|
637 |
})();
|
|
|
638 |
</script>
|