Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/fulfill.cgi
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
to 935a7c06461d
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 | #====================================================================== | |
| 14 | use strict; | |
| 15 | use warnings; | |
| 16 | ||
| 17 | use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com'; | |
| 18 | use CGI; | |
| 19 | use MODS::Template; | |
| 20 | use MODS::DBConnect; | |
| 21 | use MODS::Login; | |
| 22 | use MODS::ShopCart::Config; | |
| 23 | use MODS::ShopCart::Wrapper; | |
| 24 | use MODS::ShopCart::Permissions; | |
| 25 | use MODS::ShopCart::Shipping; | |
| 26 | ||
| 27 | $| = 1; | |
| 28 | ||
| 29 | my $q = CGI->new; | |
| 30 | my $form = $q->Vars; | |
| 31 | my $auth = MODS::Login->new; | |
| 32 | my $tfile = MODS::Template->new; | |
| 33 | my $db = MODS::DBConnect->new; | |
| 34 | my $cfg = MODS::ShopCart::Config->new; | |
| 35 | my $wrap = MODS::ShopCart::Wrapper->new; | |
| 36 | my $perm = MODS::ShopCart::Permissions->new; | |
| 37 | my $ship = MODS::ShopCart::Shipping->new; | |
| 38 | my $DB = $cfg->settings('database_name'); | |
| 39 | ||
| 40 | my $userinfo = $auth->login_verify(); | |
| 41 | unless ($userinfo) { | |
| 42 | print "Status: 302 Found\nLocation: /login.cgi?return=/fulfill.cgi\n\n"; exit; | |
| 43 | } | |
| 44 | $perm->require_feature($userinfo, 'view_orders'); | |
| 45 | ||
| 46 | my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g; | |
| 47 | my $owner_uid = $userinfo->{owner_user_id} || $uid; | |
| 48 | $owner_uid =~ s/[^0-9]//g; | |
| 49 | ||
| 50 | my $dbh = $db->db_connect(); | |
| 51 | ||
| 52 | unless ($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 | |
| 68 | my $flash_msg = ''; | |
| 69 | my $flash_kind = ''; | |
| 70 | if (defined $form->{ok}) { $flash_msg = 'Saved.'; $flash_kind = 'ok'; } | |
| 71 | elsif (defined $form->{nope}) { $flash_msg = 'Nothing to update.'; $flash_kind = 'danger'; } | |
| 72 | ||
| 73 | # POST bulk handler | |
| 74 | if (($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 | ||
| 96 | my $status_filter = $form->{status} || 'all'; | |
| 97 | $status_filter =~ s/[^a-z_]//g; | |
| 98 | my $q_filter = $form->{q} || ''; | |
| 99 | ||
| 100 | my $orders = $ship->list_orders($db, $dbh, $DB, $owner_uid, | |
| 101 | status => $status_filter, q => $q_filter); | |
| 102 | my $counts = $ship->count_by_status($db, $dbh, $DB, $owner_uid); | |
| 103 | my $settings = $ship->get_settings($db, $dbh, $DB, $owner_uid); | |
| 104 | my $needs_setup = !($settings && $settings->{origin_line1}); | |
| 105 | ||
| 106 | $db->db_disconnect($dbh); | |
| 107 | ||
| 108 | # Tab definitions in display order. | |
| 109 | my @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 | ||
| 125 | my $total = 0; $total += $_ for values %$counts; | |
| 126 | my @tabs; | |
| 127 | foreach 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 | ||
| 140 | my @rows; | |
| 141 | foreach 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 | ||
| 163 | my $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 | ||
| 176 | 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"; | |
| 177 | my $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 | }); | |
| 184 | exit; | |
| 185 | ||
| 186 | sub _h { | |
| 187 | my $s = shift // ''; | |
| 188 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; | |
| 189 | $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 190 | return $s; | |
| 191 | } | |
| 192 | sub _u { | |
| 193 | my $s = shift // ''; | |
| 194 | $s =~ s/([^A-Za-z0-9._~-])/sprintf('%%%02X', ord($1))/ge; | |
| 195 | return $s; | |
| 196 | } | |
| 197 | sub _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 | } | |
| 207 | sub _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 | } |