| Server IP : 121.121.20.254 / Your IP : 216.73.217.51 Web Server : Microsoft-IIS/10.0 System : Windows NT WEB-SERVER 10.0 build 20348 (Windows Server 2022) AMD64 User : IUSR ( 0) PHP Version : 8.3.28 Disable Function : NONE MySQL : ON | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : C:/inetpub/wwwroot/AIWEBSTATION/wp-content/novamira-sandbox/ |
Upload File : |
<?php
/** SiteGiant-style Smart Fulfillment workspace layered on AKWMS. */
if (!defined('ABSPATH')) { return; }
final class AKWMS_SiteGiant_Workspace {
const OPT_SETTINGS='akwms_sitegiant_settings';
const OPT_PICKLISTS='akwms_sitegiant_picklists';
const OPT_ESC='akwms_sitegiant_escalations';
public static function init(): void {
add_action('init',[__CLASS__,'register_shortcode'],99);
add_action('rest_api_init',[__CLASS__,'routes'],99);
}
public static function register_shortcode(): void { add_shortcode('wms_sitegiant',[__CLASS__,'shortcode']); }
public static function can_view(): bool { return current_user_can('akwms_view') || current_user_can('manage_options'); }
public static function can_manage(): bool { return current_user_can('akwms_manage_waves') || current_user_can('manage_options'); }
public static function routes(): void {
$ns='wms-sitegiant/v1';
register_rest_route($ns,'/bootstrap',['methods'=>'GET','callback'=>[__CLASS__,'bootstrap'],'permission_callback'=>[__CLASS__,'can_view']]);
register_rest_route($ns,'/test-reset',['methods'=>'POST','callback'=>[__CLASS__,'reset_test_workflow'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/settings',['methods'=>'POST','callback'=>[__CLASS__,'save_settings'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/picklists/(?P<id>\\d+)',['methods'=>'GET','callback'=>[__CLASS__,'picklist_detail'],'permission_callback'=>[__CLASS__,'can_view']]);
register_rest_route($ns,'/picklists/(?P<id>\\d+)',['methods'=>'POST','callback'=>[__CLASS__,'save_picklist'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/picklists/(?P<id>\\d+)/advance',['methods'=>'POST','callback'=>[__CLASS__,'advance_picklist'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/tasks/(?P<id>\\d+)/toggle',['methods'=>'POST','callback'=>[__CLASS__,'toggle_task'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/tasks/(?P<id>\\d+)/escalate',['methods'=>'POST','callback'=>[__CLASS__,'escalate_task'],'permission_callback'=>[__CLASS__,'can_manage']]);
register_rest_route($ns,'/escalations/(?P<id>\\d+)/resolve',['methods'=>'POST','callback'=>[__CLASS__,'resolve_escalation'],'permission_callback'=>[__CLASS__,'can_manage']]);
}
private static function defaults(): array {
return [
'default_mode'=>2,'orders_per_page'=>20,'rack_sorting'=>'rack_order','order_sorting'=>'package_id',
'mode1_sort_scan'=>'item_bin','mode2_scan'=>'bin_item','split_kit'=>0,'mode3_scan_bin'=>0,
'print_label_mode1'=>1,'convert_uom'=>0,'picklist_sort'=>'latest','scan_rack'=>0,'scan_rack_mode'=>'single',
'scan_batch'=>0,'deduct_stock'=>0,'manual_fill'=>'standard','prefix'=>'PL-','show_product_name'=>'inventory'
];
}
private static function settings(): array { return wp_parse_args((array)get_option(self::OPT_SETTINGS,[]),self::defaults()); }
private static function pickmeta(): array { return (array)get_option(self::OPT_PICKLISTS,[]); }
private static function escmeta(): array { return (array)get_option(self::OPT_ESC,[]); }
private static function users(): array {
$out=[]; foreach(get_users(['fields'=>['ID','display_name']]) as $u){$out[]=['id'=>(int)$u->ID,'name'=>$u->display_name];} return $out;
}
private static function mode_label(int $mode): string { return [1=>'Bulk Picking',2=>'Order Picking',3=>'Shelf Picking'][$mode] ?? 'Order Picking'; }
private static function wave_rows(): array {
global $wpdb; if(!class_exists('AKWMS_DB')) return [];
$w=AKWMS_DB::table('waves'); $wh=AKWMS_DB::table('warehouses'); $wo=AKWMS_DB::table('wave_orders'); $o=AKWMS_DB::table('orders'); $t=AKWMS_DB::table('tasks');
$sql="SELECT w.id,w.wave_no,w.status,w.created_at,w.updated_at,wh.code warehouse_code,
(SELECT COUNT(*) FROM $wo x WHERE x.wave_id=w.id) order_count,
(SELECT COUNT(*) FROM $t x WHERE x.wave_id=w.id) task_count,
(SELECT COUNT(*) FROM $t x WHERE x.wave_id=w.id AND x.status='COMPLETED') completed_task_count,
(SELECT COALESCE(SUM(oi.ordered_qty),0) FROM $wo xo JOIN ".AKWMS_DB::table('order_items')." oi ON oi.order_id=xo.order_id WHERE xo.wave_id=w.id) unit_count
FROM $w w LEFT JOIN $wh wh ON wh.id=w.warehouse_id ORDER BY w.id DESC LIMIT 100";
return $wpdb->get_results($sql,ARRAY_A);
}
private static function decorate_wave(array $r,array $meta,array $settings): array {
$m=$meta[(string)$r['id']] ?? [];
$mode=(int)($m['mode'] ?? $settings['default_mode']);
$tasks=(int)$r['task_count']; $done=(int)$r['completed_task_count'];
$pick=$tasks>0&&$done>=$tasks?'completed':($done>0?'in_progress':'new');
$sort=$m['sort_status'] ?? ($mode===2&&$pick==='completed'?'completed':'new');
$pack=$m['pack_status'] ?? 'new';
$pct=$tasks?round(($done/$tasks)*100):0;
return array_merge($r,[
'mode'=>$mode,'mode_label'=>self::mode_label($mode),'picker'=>(string)($m['picker']??''),'sorter'=>(string)($m['sorter']??''),
'trolley'=>(string)($m['trolley']??''),'remark'=>(string)($m['remark']??''),'pick_status'=>$pick,'sort_status'=>$sort,'pack_status'=>$pack,'progress'=>$pct
]);
}
public static function bootstrap() {
$settings=self::settings(); $meta=self::pickmeta(); $rows=[]; foreach(self::wave_rows() as $r)$rows[]=self::decorate_wave($r,$meta,$settings);
$esc=self::escmeta(); $open=array_values(array_filter($esc,fn($e)=>empty($e['resolved'])));
return ['settings'=>$settings,'picklists'=>$rows,'users'=>self::users(),'escalations'=>$open,'caps'=>['manage'=>self::can_manage()]];
}
public static function save_settings(WP_REST_Request $r) {
$d=(array)$r->get_json_params(); $s=self::settings();
$allowed=['default_mode','orders_per_page','rack_sorting','order_sorting','mode1_sort_scan','mode2_scan','split_kit','mode3_scan_bin','print_label_mode1','convert_uom','picklist_sort','scan_rack','scan_rack_mode','scan_batch','deduct_stock','manual_fill','prefix','show_product_name'];
foreach($allowed as $k) if(array_key_exists($k,$d)) $s[$k]=is_string($d[$k])?sanitize_text_field($d[$k]):$d[$k];
$s['default_mode']=max(1,min(3,(int)$s['default_mode'])); $s['orders_per_page']=max(5,min(100,(int)$s['orders_per_page']));
foreach(['split_kit','mode3_scan_bin','print_label_mode1','convert_uom','scan_rack','scan_batch','deduct_stock'] as $k)$s[$k]=empty($s[$k])?0:1;
update_option(self::OPT_SETTINGS,$s,false); return $s;
}
public static function save_picklist(WP_REST_Request $r) {
$id=(string)(int)$r['id']; $d=(array)$r->get_json_params(); $all=self::pickmeta(); $m=$all[$id]??[];
foreach(['picker','sorter','trolley','remark'] as $k) if(array_key_exists($k,$d))$m[$k]=sanitize_text_field((string)$d[$k]);
if(isset($d['mode']))$m['mode']=max(1,min(3,(int)$d['mode']));
$mode=(int)($m['mode']??self::settings()['default_mode']);
$container=trim((string)($m['trolley']??''));
if($mode===1 && $container==='') return new WP_Error('trolley_required','Mode 1 Bulk Picking requires a picking trolley',['status'=>409]);
if($mode===2 && $container==='') return new WP_Error('container_required','Mode 2 Order Picking requires a picking trolley or picking bin',['status'=>409]);
$all[$id]=$m; update_option(self::OPT_PICKLISTS,$all,false); return $m;
}
public static function picklist_detail(WP_REST_Request $r) {
global $wpdb; $id=(int)$r['id']; if(!class_exists('AKWMS_DB'))return new WP_Error('wms_missing','WMS plugin is unavailable',['status'=>503]);
$wave=$wpdb->get_row($wpdb->prepare("SELECT w.*,wh.code warehouse_code FROM ".AKWMS_DB::table('waves')." w LEFT JOIN ".AKWMS_DB::table('warehouses')." wh ON wh.id=w.warehouse_id WHERE w.id=%d",$id),ARRAY_A);
if(!$wave)return new WP_Error('not_found','Pick list not found',['status'=>404]);
$tasks=$wpdb->get_results($wpdb->prepare("SELECT t.id,t.task_no,t.status,t.requested_qty,t.confirmed_qty,t.priority,s.sku,s.name sku_name,l.code rack_code,p.code lpn_code FROM ".AKWMS_DB::table('tasks')." t LEFT JOIN ".AKWMS_DB::table('skus')." s ON s.id=t.sku_id LEFT JOIN ".AKWMS_DB::table('locations')." l ON l.id=t.from_location_id LEFT JOIN ".AKWMS_DB::table('lpns')." p ON p.id=t.lpn_id WHERE t.wave_id=%d ORDER BY l.code,s.sku,t.id",$id),ARRAY_A);
$orders=$wpdb->get_results($wpdb->prepare("SELECT o.id,o.order_no,o.customer_name,o.status,o.source_channel FROM ".AKWMS_DB::table('wave_orders')." wo JOIN ".AKWMS_DB::table('orders')." o ON o.id=wo.order_id WHERE wo.wave_id=%d ORDER BY o.id",$id),ARRAY_A);
$meta=self::pickmeta(); $decor=self::decorate_wave(array_merge($wave,['order_count'=>count($orders),'task_count'=>count($tasks),'completed_task_count'=>count(array_filter($tasks,fn($t)=>$t['status']==='COMPLETED')),'unit_count'=>array_sum(array_map(fn($t)=>(float)$t['requested_qty'],$tasks))]),$meta,self::settings());
$esc=self::escmeta(); foreach($tasks as &$t){$t['escalation']=$esc[(string)$t['id']]??null;} unset($t);
return ['picklist'=>$decor,'tasks'=>$tasks,'orders'=>$orders];
}
public static function toggle_task(WP_REST_Request $r) {
global $wpdb; $id=(int)$r['id']; $d=(array)$r->get_json_params(); $complete=!empty($d['complete']); $t=AKWMS_DB::table('tasks');
$row=$wpdb->get_row($wpdb->prepare("SELECT * FROM $t WHERE id=%d",$id),ARRAY_A); if(!$row)return new WP_Error('not_found','Pick task not found',['status'=>404]);
if(!$complete && $row['status']==='COMPLETED') return new WP_Error('status_locked','Completed picking cannot be returned to New/Pending. Create a correction or escalation instead.',['status'=>409]);
$data=$complete?['status'=>'COMPLETED','confirmed_qty'=>(float)$row['requested_qty'],'completed_at'=>current_time('mysql')]:['status'=>'PENDING','confirmed_qty'=>0,'completed_at'=>null];
$wpdb->update($t,$data,['id'=>$id]); if(class_exists('AKWMS_Audit'))AKWMS_Audit::log($complete?'sitegiant_pick_complete':'sitegiant_pick_reopen','task',$id,$complete?'Pick task completed':'Pick task reopened');
return ['id'=>$id,'status'=>$data['status']];
}
public static function advance_picklist(WP_REST_Request $r) {
$id=(string)(int)$r['id']; $d=(array)$r->get_json_params(); $stage=sanitize_key($d['stage']??''); $all=self::pickmeta(); $m=$all[$id]??[];
if($stage==='sort')$m['sort_status']='completed'; elseif($stage==='pack')$m['pack_status']='completed'; else return new WP_Error('bad_stage','Stage must be sort or pack',['status'=>400]);
$all[$id]=$m; update_option(self::OPT_PICKLISTS,$all,false); return $m;
}
public static function escalate_task(WP_REST_Request $r) {
$id=(string)(int)$r['id']; $d=(array)$r->get_json_params(); $note=trim(sanitize_textarea_field($d['note']??'')); if($note==='')return new WP_Error('note_required','Describe the picking problem',['status'=>400]);
$all=self::escmeta(); $all[$id]=['id'=>(int)$id,'note'=>$note,'resolved'=>false,'created_at'=>current_time('mysql'),'user'=>wp_get_current_user()->display_name]; update_option(self::OPT_ESC,$all,false); return $all[$id];
}
public static function resolve_escalation(WP_REST_Request $r) {
$id=(string)(int)$r['id']; $all=self::escmeta(); if(!isset($all[$id]))return new WP_Error('not_found','Escalation not found',['status'=>404]); $all[$id]['resolved']=true; $all[$id]['resolved_at']=current_time('mysql'); update_option(self::OPT_ESC,$all,false); return $all[$id];
}
public static function reset_test_workflow(WP_REST_Request $r) {
global $wpdb;
if(!class_exists('AKWMS_DB')) return new WP_Error('wms_missing','WMS database is unavailable',['status'=>503]);
$d=(array)$r->get_json_params();
$sku=strtoupper(trim(sanitize_text_field($d['sku']??'')));
$customer=trim(sanitize_text_field($d['customer']??''));
$full=!empty($d['full']);
if($sku==='' || strpos($sku,'SG-TEST-')!==0) return new WP_Error('bad_test_sku','Only SG-TEST-* workflow data can be reset',['status'=>400]);
$t=fn(string $name)=>AKWMS_DB::table($name);
$sku_id=(int)$wpdb->get_var($wpdb->prepare("SELECT id FROM {$t('skus')} WHERE sku=%s",$sku));
$order_ids=[];
if($customer!=='') $order_ids=array_map('intval',$wpdb->get_col($wpdb->prepare("SELECT id FROM {$t('orders')} WHERE customer_name=%s",$customer)));
if(!$order_ids && $sku_id) $order_ids=array_map('intval',$wpdb->get_col($wpdb->prepare("SELECT DISTINCT order_id FROM {$t('order_items')} WHERE sku_id=%d",$sku_id)));
$wave_ids=[];
if($order_ids){$in=implode(',',array_fill(0,count($order_ids),'%d'));$wave_ids=array_map('intval',$wpdb->get_col($wpdb->prepare("SELECT DISTINCT wave_id FROM {$t('wave_orders')} WHERE order_id IN ($in)",...$order_ids)));}
if($wave_ids){
$in=implode(',',array_fill(0,count($wave_ids),'%d'));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('tasks')} WHERE wave_id IN ($in)",...$wave_ids));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('wave_orders')} WHERE wave_id IN ($in)",...$wave_ids));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('waves')} WHERE id IN ($in)",...$wave_ids));
$meta=self::pickmeta(); foreach($wave_ids as $wid) unset($meta[(string)$wid]); update_option(self::OPT_PICKLISTS,$meta,false);
}
if($order_ids){
$in=implode(',',array_fill(0,count($order_ids),'%d'));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('reservations')} WHERE order_id IN ($in)",...$order_ids));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('inventory_transactions')} WHERE order_id IN ($in)",...$order_ids));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('order_items')} WHERE order_id IN ($in)",...$order_ids));
$wpdb->query($wpdb->prepare("DELETE FROM {$t('orders')} WHERE id IN ($in)",...$order_ids));
}
if($sku_id){
$inv_ids=array_map('intval',$wpdb->get_col($wpdb->prepare("SELECT id FROM {$t('inventory')} WHERE sku_id=%d",$sku_id)));
if($inv_ids){$in=implode(',',array_fill(0,count($inv_ids),'%d'));$wpdb->query($wpdb->prepare("DELETE FROM {$t('cycle_counts')} WHERE inventory_id IN ($in)",...$inv_ids));}
$wpdb->delete($t('inventory_transactions'),['sku_id'=>$sku_id],['%d']);
$wpdb->delete($t('inventory'),['sku_id'=>$sku_id],['%d']);
$wpdb->delete($t('skus'),['id'=>$sku_id],['%d']);
}
if($full){
$wh_id=(int)$wpdb->get_var($wpdb->prepare("SELECT id FROM {$t('warehouses')} WHERE code=%s",'SGTEST'));
if($wh_id){
$wpdb->delete($t('locations'),['warehouse_id'=>$wh_id],['%d']);
$wpdb->delete($t('warehouses'),['id'=>$wh_id],['%d']);
}
$containers=(array)get_option('akwms_sitegiant_containers',[]); unset($containers['sg-trolley-01']); update_option('akwms_sitegiant_containers',$containers,false);
}
return ['reset'=>true,'full'=>$full,'sku'=>$sku,'removed_orders'=>count($order_ids),'removed_waves'=>count($wave_ids)];
}
public static function shortcode(): string {
if(!is_user_logged_in())return '<div class="sgw-shell"><div class="sgw-access"><h1>SiteGiant WMS</h1><p>Sign in with an authorized warehouse account.</p><a class="sgw-btn" href="'.esc_url(wp_login_url(get_permalink())).'">Sign in</a></div></div>';
if(!self::can_view())return '<div class="sgw-shell"><div class="sgw-access"><h1>SiteGiant WMS</h1><p>Your account does not have WMS access.</p></div></div>';
$css=WP_CONTENT_DIR.'/novamira-sandbox/wms-sitegiant.css'; $js=WP_CONTENT_DIR.'/novamira-sandbox/wms-sitegiant.js';
wp_enqueue_style('akwms-sitegiant',content_url('novamira-sandbox/wms-sitegiant.css'),[],file_exists($css)?(string)filemtime($css):'1');
wp_enqueue_script('akwms-sitegiant',content_url('novamira-sandbox/wms-sitegiant.js'),['wp-api-fetch'],file_exists($js)?(string)filemtime($js):'1',true);
wp_localize_script('akwms-sitegiant','SGW',['root'=>esc_url_raw(rest_url('wms-sitegiant/v1/')),'nonce'=>wp_create_nonce('wp_rest')]);
ob_start(); ?>
<div id="sgw-app" class="sgw-shell">
<header class="sgw-top"><div><p class="sgw-kicker">SiteGiant WMS</p><h1>WMS Workspace - SiteGiant</h1><p>Test the full warehouse lifecycle from stock-in and storage to picking, sorting, packing and stock-out.</p></div><div class="sgw-top-actions"><button class="sgw-btn sgw-secondary" data-refresh>Refresh</button></div></header>
<div id="sgw-msg" class="sgw-msg" role="status"></div>
<div id="sgw-native" class="sgw-native" aria-live="polite"></div>
<div id="sgw-drawer" class="sgw-drawer" hidden><div class="sgw-drawer-backdrop" data-close></div><aside class="sgw-drawer-panel"><button class="sgw-drawer-close" data-close aria-label="Close">×</button><div id="sgw-drawer-content"></div></aside></div>
</div><?php return (string)ob_get_clean();
}
}
AKWMS_SiteGiant_Workspace::init();