Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/fulfill.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/shop.3dshawn.com/fulfill.cgi

added on local at 2026-07-11 18:36:33

Added
+223
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 935a7c06461d
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ShopCart -- Fulfillment list.
4#
5# Seller-facing page that lists every PHYSICAL order (purchase_kind=
6# physical on at least one order_items row) the seller owns. Status
7# tabs let the seller drill into one bucket at a time; a free-text
8# search filters by buyer email / ship-to name / order id.
9#
10# Per-row CTA -> /fulfill_order.cgi?order_id=N for the detail view.
11# Bulk actions (mark printing, mark shipped, etc.) POST here with
12# _act=bulk and a list of order_ids.
13#======================================================================
14use strict;
15use warnings;
16
17use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
18use CGI;
19use MODS::Template;
20use MODS::DBConnect;
21use MODS::Login;
22use MODS::ShopCart::Config;
23use MODS::ShopCart::Wrapper;
24use MODS::ShopCart::Permissions;
25use MODS::ShopCart::Shipping;
26
27$| = 1;
28
29my $q = CGI->new;
30my $form = $q->Vars;
31my $auth = MODS::Login->new;
32my $tfile = MODS::Template->new;
33my $db = MODS::DBConnect->new;
34my $cfg = MODS::ShopCart::Config->new;
35my $wrap = MODS::ShopCart::Wrapper->new;
36my $perm = MODS::ShopCart::Permissions->new;
37my $ship = MODS::ShopCart::Shipping->new;
38my $DB = $cfg->settings('database_name');
39
40my $userinfo = $auth->login_verify();
41unless ($userinfo) {
42 print "Status: 302 Found\nLocation: /login.cgi?return=/fulfill.cgi\n\n"; exit;
43}
44$perm->require_feature($userinfo, 'view_orders');
45
46my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
47my $owner_uid = $userinfo->{owner_user_id} || $uid;
48$owner_uid =~ s/[^0-9]//g;
49
50my $dbh = $db->db_connect();
51
52unless ($ship->schema_ready($db, $dbh, $DB)) {
53 $db->db_disconnect($dbh);
54 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
55 $wrap->render({
56 userinfo => $userinfo,
57 page_key => 'fulfillment',
58 title => 'Fulfill Orders',
59 body => qq~<div class="module"><div class="module-body" style="padding:40px;text-align:center">
60 <h1 style="font-family:var(--font-display);color:#fff">Fulfillment not installed yet</h1>
61 <p style="color:var(--col-text-2);margin-top:12px">Run the fulfillment migration from <code>installation_instructions.html</code>, then refresh.</p>
62 </div></div>~,
63 });
64 exit;
65}
66
67# Flash
68my $flash_msg = '';
69my $flash_kind = '';
70if (defined $form->{ok}) { $flash_msg = 'Saved.'; $flash_kind = 'ok'; }
71elsif (defined $form->{nope}) { $flash_msg = 'Nothing to update.'; $flash_kind = 'danger'; }
72
73# POST bulk handler
74if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
75 my $act = lc($form->{_act} || ''); $act =~ s/[^a-z_]//g;
76 if ($act eq 'bulk') {
77 my $new_status = $form->{new_status} || '';
78 $new_status =~ s/[^a-z_]//g;
79 # CGI->Vars collapses multi-value fields into NUL-separated strings.
80 my $ids_raw = $form->{order_ids} || '';
81 my @ids = split /\0+/, $ids_raw;
82 my $n = 0;
83 foreach my $id (@ids) {
84 $id =~ s/[^0-9]//g; next unless length $id;
85 $ship->update_order_status($db, $dbh, $DB, $id, $owner_uid, $new_status, cascade_items => 1, by => $userinfo->{display_name});
86 $n++;
87 }
88 $db->db_disconnect($dbh);
89 my $tail = $n ? 'ok=1' : 'nope=1';
90 print "Status: 302 Found\nLocation: /fulfill.cgi?$tail\n\n"; exit;
91 }
92 $db->db_disconnect($dbh);
93 print "Status: 302 Found\nLocation: /fulfill.cgi\n\n"; exit;
94}
95
96my $status_filter = $form->{status} || 'all';
97$status_filter =~ s/[^a-z_]//g;
98my $q_filter = $form->{q} || '';
99
100my $orders = $ship->list_orders($db, $dbh, $DB, $owner_uid,
101 status => $status_filter, q => $q_filter);
102my $counts = $ship->count_by_status($db, $dbh, $DB, $owner_uid);
103my $settings = $ship->get_settings($db, $dbh, $DB, $owner_uid);
104my $needs_setup = !($settings && $settings->{origin_line1});
105
106$db->db_disconnect($dbh);
107
108# Tab definitions in display order.
109my @TABS = (
110 [ 'all', 'All', '' ],
111 [ 'new', 'New', 'A new order arrived, ready to start processing.' ],
112 [ 'processing', 'Processing', 'Preparing the order (queuing, slicing, supplies on hand).' ],
113 [ 'printing', 'Printing', 'Currently on the printer.' ],
114 [ 'printed', 'Printed', 'Off the printer, waiting to be cleaned and packed.' ],
115 [ 'packed', 'Packed', 'In a box, ready for the carrier.' ],
116 [ 'shipped', 'Shipped', 'Handed to the carrier with a tracking number.' ],
117 [ 'partially_shipped', 'Partial', 'Multi-item order; only some items shipped.' ],
118 [ 'delivered', 'Delivered', 'Carrier reported delivery.' ],
119 [ 'completed', 'Completed', 'Closed out. No further action needed.' ],
120 [ 'on_hold', 'On hold', 'Waiting on buyer confirmation, missing info, etc.' ],
121 [ 'cancelled', 'Cancelled', 'Order cancelled.' ],
122 [ 'returned', 'Returned', 'Buyer returned the package.' ],
123);
124
125my $total = 0; $total += $_ for values %$counts;
126my @tabs;
127foreach my $t (@TABS) {
128 my ($key, $label, $hint) = @$t;
129 my $n = $key eq 'all' ? $total : ($counts->{$key} || 0);
130 push @tabs, {
131 key => $key,
132 label => $label,
133 hint => $hint,
134 count => $n,
135 active => ($status_filter eq $key) ? 1 : 0,
136 href => '/fulfill.cgi?status=' . $key . (length $q_filter ? '&q=' . _u($q_filter) : ''),
137 };
138}
139
140my @rows;
141foreach my $o (@$orders) {
142 my $ship_to = join(', ', grep { length } (
143 $o->{ship_name} || '',
144 $o->{ship_city} || '',
145 $o->{ship_region} || '',
146 $o->{ship_country} || '',
147 ));
148 push @rows, {
149 id => $o->{id},
150 buyer_email => _h($o->{buyer_email}),
151 ship_to => _h($ship_to),
152 status => $o->{fulfillment_status} || 'new',
153 status_label => _status_label($o->{fulfillment_status} || 'new'),
154 status_class => 'fs-' . ($o->{fulfillment_status} || 'new'),
155 total => sprintf('%.2f', ($o->{total_amount_cents} || 0) / 100),
156 currency => $o->{currency} || 'USD',
157 shipping => sprintf('%.2f', ($o->{shipping_cents} || 0) / 100),
158 created_at => _flf_ts_span($o->{created_at}, ''),
159 href => '/fulfill_order.cgi?order_id=' . $o->{id},
160 };
161}
162
163my $tvars = {
164 has_rows => scalar(@rows) ? 1 : 0,
165 rows => \@rows,
166 tabs => \@tabs,
167 q => _h($q_filter),
168 status_filter => $status_filter,
169 flash_msg => $flash_msg,
170 flash_kind => $flash_kind,
171 has_flash => length $flash_msg ? 1 : 0,
172 needs_setup => $needs_setup ? 1 : 0,
173 row_count => scalar(@rows),
174};
175
176print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
177my $body = join('', $tfile->template('shopcart_fulfill.html', $tvars, $userinfo));
178$wrap->render({
179 userinfo => $userinfo,
180 page_key => 'fulfillment',
181 title => 'Fulfill Orders',
182 body => $body,
183});
184exit;
185
186sub _h {
187 my $s = shift // '';
188 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
189 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
190 return $s;
191}
192sub _u {
193 my $s = shift // '';
194 $s =~ s/([^A-Za-z0-9._~-])/sprintf('%%%02X', ord($1))/ge;
195 return $s;
196}
197sub _flf_ts_span {
198 my ($s, $fallback) = @_;
199 $fallback = '' unless defined $fallback;
200 return $fallback unless $s && length $s;
201 return $s unless $s =~ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/;
202 require Time::Local;
203 my $ep = eval { Time::Local::timelocal($6,$5,$4,$3,$2-1,$1-1900) } || 0;
204 return $s unless $ep;
205 return qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~;
206}
207sub _status_label {
208 my $s = shift;
209 return {
210 new => 'New',
211 processing => 'Processing',
212 printing => 'Printing',
213 printed => 'Printed',
214 packed => 'Packed',
215 shipped => 'Shipped',
216 partially_shipped => 'Partially shipped',
217 delivered => 'Delivered',
218 completed => 'Completed',
219 on_hold => 'On hold',
220 cancelled => 'Cancelled',
221 returned => 'Returned',
222 }->{$s || ''} || ucfirst($s || 'New');
223}