diff options
| author | Lester Caine <lester@lsces.co.uk> | 2026-06-08 20:56:56 +0100 |
|---|---|---|
| committer | Lester Caine <lester@lsces.co.uk> | 2026-06-08 20:56:56 +0100 |
| commit | a838a2d3963738b2e9bc496b31e078ead79d4a43 (patch) | |
| tree | bafe061903e4a076b92eb6d00813661f21f64ae8 | |
| parent | 5e0af09c649b90a0963bd4e6b62f24b8cbf8edd9 (diff) | |
| download | stock-a838a2d3963738b2e9bc496b31e078ead79d4a43.tar.gz stock-a838a2d3963738b2e9bc496b31e078ead79d4a43.tar.bz2 stock-a838a2d3963738b2e9bc496b31e078ead79d4a43.zip | |
stock: requisition system, movement BOM editor, autocomplete dropdowns
Requisitions:
- add_requisition.php: create REQN movements linked to assemblies or
kitlocker components; Ordered date stored on REQN xref start_date
- ASSEMBLY xref group registered in schema (sort_order=1, template=assembly);
ASSEMBLY item registered with multiple=1 for future multi-item reqns
- StockMovement::loadXrefInfo() enriches assembly group rows with
linked_title/linked_desc from liberty_content
- view_xref_assembly_group/item templates: Assembly tab with qty, title
link, type; Add item form on edit_movement for REQNs; hidden on I-direction
- view_movement/edit_movement: isReqn flag hides assembly tab and type
selector on ORDER/TRANS; add another assembly from edit_movement
BOM editor:
- stockmovement/view_xref_bom_group: Add component links to new
add_movement_component.php instead of generic liberty/add_xref.php
- add_movement_component.php/.tpl: movement-aware add component page;
uses movement_lookup_inc, STOCKMOVEMENT_CONTENT_TYPE_GUID, sequential
xorder, redirects back to edit_movement
Autocomplete dropdowns (replace <select> everywhere):
- add_requisition: hidden+text+dropdown widget; KLID in JSON for matching
- add_supplier: AJAX contact lookup replaces static supplier list
- list_stock: assembly selector replaced with autocomplete dropdown
- edit_movement: From field proper dropdown replacing datalist;
Movement Type radio fixed (was reading non-existent mInfo[reference])
- view_xref_assembly_group: Add item inline form with same widget
Import:
- ImportKitlockerAssemblies: upsert xrefs on reload; KLID/KLSGL/KL3M
now stored on both assemblies and components (not assembly-only)
- StockComponent::getList: kitlocker_only filter via EXISTS(KLID xref)
list_movements: Add Requisition icon alongside Add Movement in header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | add_movement_component.php | 85 | ||||
| -rw-r--r-- | add_requisition.php | 104 | ||||
| -rw-r--r-- | add_supplier.php | 7 | ||||
| -rwxr-xr-x | admin/schema_inc.php | 4 | ||||
| -rw-r--r-- | edit_movement.php | 74 | ||||
| -rw-r--r-- | import/ImportKitlockerAssemblies.php | 110 | ||||
| -rwxr-xr-x | includes/classes/StockComponent.php | 4 | ||||
| -rw-r--r-- | includes/classes/StockMovement.php | 17 | ||||
| -rw-r--r-- | list_stock.php | 18 | ||||
| -rw-r--r-- | templates/add_movement_component.tpl | 113 | ||||
| -rw-r--r-- | templates/add_requisition.tpl | 90 | ||||
| -rw-r--r-- | templates/add_supplier.tpl | 67 | ||||
| -rw-r--r-- | templates/edit_movement.tpl | 107 | ||||
| -rw-r--r-- | templates/list_movements.tpl | 1 | ||||
| -rw-r--r-- | templates/list_stock.tpl | 59 | ||||
| -rw-r--r-- | templates/stockmovement/view_xref_bom_group.tpl | 2 | ||||
| -rw-r--r-- | templates/view_movement.tpl | 2 | ||||
| -rw-r--r-- | templates/view_xref_assembly_group.tpl | 113 | ||||
| -rw-r--r-- | templates/view_xref_assembly_item.tpl | 24 | ||||
| -rw-r--r-- | view_movement.php | 1 |
20 files changed, 848 insertions, 154 deletions
diff --git a/add_movement_component.php b/add_movement_component.php new file mode 100644 index 0000000..9f2dfa2 --- /dev/null +++ b/add_movement_component.php @@ -0,0 +1,85 @@ +<?php +/** + * Add a single component line to a stock movement BOM. + * Same flow as add_component.php but for stockmovement content. + * + * @package stock + */ + +namespace Bitweaver\Stock; + +use Bitweaver\KernelTools; +use Bitweaver\Liberty\LibertyXref; + +require_once '../kernel/includes/setup_inc.php'; + +global $gBitSystem, $gBitSmarty, $gBitDb; + +include_once STOCK_PKG_INCLUDE_PATH.'movement_lookup_inc.php'; + +if( !$gContent->isValid() ) { + $gBitSystem->fatalError( 'No valid movement specified.' ); +} +$gContent->verifyUpdatePermission(); + +$validItems = [ 'SGL' => 'Single unit', 'PCK' => 'Pack', 'SHT' => 'Sheet (H x W)', 'VOL' => 'Volume' ]; +$errors = []; + +if( !empty( $_REQUEST['fCancel'] ) ) { + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); + die; +} + +if( !empty( $_REQUEST['fAddComponent'] ) ) { + $title = trim( $_REQUEST['component_title'] ?? '' ); + $item = strtoupper( trim( $_REQUEST['item'] ?? 'SGL' ) ); + $xkey = trim( $_REQUEST['xkey'] ?? '' ); + + if( !array_key_exists( $item, $validItems ) ) { + $item = 'SGL'; + } + + if( $title === '' ) { + $errors[] = KernelTools::tra( 'Component title is required.' ); + } else { + $compId = (int)$gBitDb->getOne( + "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc + WHERE lc.`content_type_guid` = 'stockcomponent' AND lc.`title` = ?", + [ $title ] + ); + + if( !$compId ) { + header( 'Location: '.STOCK_PKG_URL.'edit_component.php?title='.urlencode( $title ) ); + die; + } + + $nextXorder = (int)$gBitDb->getOne( + "SELECT COALESCE( MAX(x.`xorder`) + 1, 1 ) FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`content_id` = ? AND x.`item` IN ('SGL','PCK','SHT','VOL')", + [ $gContent->mContentId ] + ) ?: 1; + + $xrefObj = new LibertyXref(); + $xrefObj->mContentTypeGuid = 'stockmovement'; + $pHash = [ + 'content_id' => $gContent->mContentId, + 'item' => $item, + 'xorder' => $nextXorder, + 'xref' => $compId, + 'xkey' => $xkey, + 'xkey_ext' => trim( $_REQUEST['xkey_ext'] ?? '' ) ?: null, + 'edit' => trim( $_REQUEST['edit'] ?? '' ) ?: null, + ]; + if( $xrefObj->store( $pHash ) ) { + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); + die; + } + $errors[] = KernelTools::tra( 'Failed to store BOM entry.' ); + } +} + +$gBitSmarty->assign( 'validItems', $validItems ); +$gBitSmarty->assign( 'errors', $errors ); +$gBitSmarty->assign( 'lookupUrl', STOCK_PKG_URL.'includes/lookup_component.php' ); + +$gBitSystem->display( 'bitpackage:stock/add_movement_component.tpl', KernelTools::tra( 'Add Component' ).': '.$gContent->getTitle(), [ 'display_mode' => 'edit' ] ); diff --git a/add_requisition.php b/add_requisition.php index db9a9b7..9f8f58f 100644 --- a/add_requisition.php +++ b/add_requisition.php @@ -10,7 +10,7 @@ use Bitweaver\KernelTools; require_once '../kernel/includes/setup_inc.php'; -global $gBitSystem, $gBitSmarty, $gBitUser; +global $gBitSystem, $gBitSmarty, $gBitUser, $gBitDb; $gBitSystem->verifyPermission( 'p_stock_create' ); @@ -20,27 +20,27 @@ if( !empty( $_REQUEST['fCancel'] ) ) { } if( !empty( $_REQUEST['fCreate'] ) ) { - $assemblyContentId = isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST['assembly_content_id'] ) - ? (int)$_REQUEST['assembly_content_id'] : null; - $kitCount = isset( $_REQUEST['kit_count'] ) && is_numeric( $_REQUEST['kit_count'] ) && (float)$_REQUEST['kit_count'] > 0 - ? (float)$_REQUEST['kit_count'] : 1; + $targetContentId = isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST['assembly_content_id'] ) + ? (int)$_REQUEST['assembly_content_id'] : null; + $kitCount = isset( $_REQUEST['kit_count'] ) && is_numeric( $_REQUEST['kit_count'] ) && (float)$_REQUEST['kit_count'] > 0 + ? (float)$_REQUEST['kit_count'] : 1; $title = trim( $_REQUEST['title'] ?? '' ); - if( !$assemblyContentId ) { - $errors[] = KernelTools::tra( 'Please select an assembly.' ); + if( !$targetContentId ) { + $errors[] = KernelTools::tra( 'Please select an assembly or component.' ); } elseif( $title === '' ) { $errors[] = KernelTools::tra( 'Please enter an RQ number.' ); } else { - // Verify assembly exists without relying on content type registry - global $gBitDb; - $assemblyExists = $gBitDb->getOne( - "SELECT `content_id` FROM `".BIT_DB_PREFIX."liberty_content` - WHERE `content_id` = ? AND `content_type_guid` = 'stockassembly'", - [ $assemblyContentId ] + $targetRow = $gBitDb->getRow( + "SELECT `content_type_guid`, `title` FROM `".BIT_DB_PREFIX."liberty_content` + WHERE `content_id` = ? AND `content_type_guid` IN ('stockassembly','stockcomponent')", + [ $targetContentId ] ); - if( !$assemblyExists ) { - $errors[] = KernelTools::tra( 'Assembly not found.' ); + $targetGuid = $targetRow['content_type_guid'] ?? null; + $targetTitle = $targetRow['title'] ?? ''; + if( !$targetGuid ) { + $errors[] = KernelTools::tra( 'Item not found.' ); } else { $movement = new StockMovement(); $paramHash = [ @@ -54,15 +54,40 @@ if( !empty( $_REQUEST['fCreate'] ) ) { 'xkey' => $title, 'fAddXref' => 1, ]; + $orderedDateStr = trim( $_REQUEST['ordered_date'] ?? '' ); + if( $orderedDateStr !== '' ) { + $parts = explode( '/', $orderedDateStr ); + if( count( $parts ) === 3 ) { + $year = (int)$parts[2] < 100 ? 2000 + (int)$parts[2] : (int)$parts[2]; + $ts = mktime( 0, 0, 0, (int)$parts[1], (int)$parts[0], $year ); + if( $ts ) $reqnHash['start_date'] = $ts; + } + } $movement->storeXref( $reqnHash ); $assemblyHash = [ 'content_id' => $movement->mContentId, 'item' => 'ASSEMBLY', - 'xref' => $assemblyContentId, + 'xref' => $targetContentId, + 'xkey' => (string)$kitCount, + 'xkey_ext' => $targetTitle, + 'edit' => $targetGuid, 'fAddXref' => 1, ]; $movement->storeXref( $assemblyHash ); - $movement->explodeFromAssembly( $assemblyContentId, $kitCount ); + if( $targetGuid === 'stockassembly' ) { + $movement->explodeFromAssembly( $targetContentId, $kitCount ); + } else { + // Single component — one SGL quantity row + $qtyHash = [ + 'content_id' => $movement->mContentId, + 'item' => 'SGL', + 'xref' => $targetContentId, + 'xkey' => (string)$kitCount, + 'xorder' => 1, + 'fAddXref' => 1, + ]; + $movement->storeXref( $qtyHash ); + } header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$movement->mContentId ); die; } @@ -71,10 +96,17 @@ if( !empty( $_REQUEST['fCreate'] ) ) { } } -// Assembly list for selector -$assembly = new StockAssembly(); -$listHash = [ 'show_empty' => true, 'sort_mode' => 'title_asc', 'max_records' => 500 ]; -$assemblyList = $assembly->getList( $listHash ); +// Assemblies and components merged into one flat alphabetical list +$assembly = new StockAssembly(); +$asmHash = [ 'show_empty' => true, 'sort_mode' => 'title_asc', 'max_records' => 1000 ]; +$assemblyList = $assembly->getList( $asmHash ); + +$component = new StockComponent(); +$compHash = [ 'kitlocker_only' => true, 'sort_mode' => 'title_asc', 'max_records' => 1000 ]; +$componentList = $component->getList( $compHash ); + +$itemList = array_merge( array_values( $assemblyList ), array_values( $componentList ) ); +usort( $itemList, fn( $a, $b ) => strcasecmp( $a['title'], $b['title'] ) ); // Pre-select if coming from list_stock $preselect = isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST['assembly_content_id'] ) @@ -82,9 +114,31 @@ $preselect = isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST[ $kitCount = isset( $_REQUEST['kit_count'] ) && is_numeric( $_REQUEST['kit_count'] ) ? (float)$_REQUEST['kit_count'] : 1; -$gBitSmarty->assign( 'assemblyList', $assemblyList ); -$gBitSmarty->assign( 'preselect', $preselect ); -$gBitSmarty->assign( 'kitCount', $kitCount ); -$gBitSmarty->assign( 'errors', $errors ?? [] ); +$itemIds = array_column( $itemList, 'content_id' ); +$klidMap = []; +if( $itemIds ) { + $klidRows = $gBitDb->getAll( + "SELECT x.`content_id`, x.`xkey` FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`item` = 'KLID' AND x.`content_id` IN (".implode( ',', array_fill( 0, count( $itemIds ), '?' ) ).")", + $itemIds + ); + foreach( $klidRows as $r ) { $klidMap[$r['content_id']] = $r['xkey']; } +} +$itemListJson = json_encode( array_map( + fn( $i ) => [ 'id' => (int)$i['content_id'], 'text' => $i['title'], 'klid' => $klidMap[$i['content_id']] ?? '' ], + $itemList +) ); +$preselectTitle = ''; +if( $preselect ) { + foreach( $itemList as $item ) { + if( (int)$item['content_id'] === $preselect ) { $preselectTitle = $item['title']; break; } + } +} +$gBitSmarty->assign( 'itemListJson', $itemListJson ); +$gBitSmarty->assign( 'preselect', $preselect ); +$gBitSmarty->assign( 'preselectTitle', $preselectTitle ); +$gBitSmarty->assign( 'kitCount', $kitCount ); +$gBitSmarty->assign( 'todayFormatted', date( 'd/m/Y' ) ); +$gBitSmarty->assign( 'errors', $errors ?? [] ); $gBitSystem->display( 'bitpackage:stock/add_requisition.tpl', KernelTools::tra( 'Create Requisition' ), [ 'display_mode' => 'edit' ] ); diff --git a/add_supplier.php b/add_supplier.php index f834380..8235311 100644 --- a/add_supplier.php +++ b/add_supplier.php @@ -6,7 +6,6 @@ namespace Bitweaver\Stock; -use Bitweaver\Contact\Contact; use Bitweaver\KernelTools; require_once '../kernel/includes/setup_inc.php'; @@ -48,11 +47,7 @@ if( !empty( $_REQUEST['fAddSupplier'] ) ) { } } -$contact = new Contact(); -$listHash = [ 'contact_type_guid' => ['$04'], 'sort_mode' => 'title_asc', 'max_records' => 500 ]; -$supplierList = $contact->getList( $listHash ); - -$gBitSmarty->assign( 'supplierList', $supplierList ); +$gBitSmarty->assign( 'contactLookupUrl', CONTACT_PKG_URL.'includes/lookup_contact.php' ); $gBitSmarty->assign( 'errors', $gContent->mErrors ); $gBitSystem->display( 'bitpackage:stock/add_supplier.tpl', KernelTools::tra( 'Add Supplier' ), [ 'display_mode' => 'edit' ] ); diff --git a/admin/schema_inc.php b/admin/schema_inc.php index 8db4e41..0e42764 100755 --- a/admin/schema_inc.php +++ b/admin/schema_inc.php @@ -161,8 +161,12 @@ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`, // stockmovement xref groups $xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`) VALUES ('reference','stockmovement','Reference',1,3,'')"; +$xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`,`template`) VALUES ('assembly','stockmovement','Assembly',1,3,'','assembly')"; $xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`,`template`) VALUES ('quantity','stockmovement','Items',2,3,'','bom')"; +// assembly item — links the movement to its assembly or component (multiple=1 for future multi-item reqns) +$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('ASSEMBLY','stockmovement','assembly','Assembly',1,3,'','assembly',NULL)"; + // reference items — REQN=out, TRANS=in from elf, ORDER=in from supplier $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('REQN', 'stockmovement','reference','Requisition',1,3,'','text',NULL)"; $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('TRANS','stockmovement','reference','Transfer', 1,3,'','text',NULL)"; diff --git a/edit_movement.php b/edit_movement.php index 2f686d4..816553c 100644 --- a/edit_movement.php +++ b/edit_movement.php @@ -107,6 +107,51 @@ if( !empty( $_REQUEST['fSave'] ) ) { $gBitSmarty->assign( 'csvErrors', $csvResult['errors'] ); } +} elseif( !empty( $_REQUEST['fAddAssembly'] ) && $gContent->isValid() ) { + $targetContentId = isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST['assembly_content_id'] ) + ? (int)$_REQUEST['assembly_content_id'] : 0; + $kitCount = isset( $_REQUEST['kit_count'] ) && is_numeric( $_REQUEST['kit_count'] ) && (float)$_REQUEST['kit_count'] > 0 + ? (float)$_REQUEST['kit_count'] : 1; + if( $targetContentId ) { + $targetRow = $gBitDb->getRow( + "SELECT `content_type_guid`, `title` FROM `".BIT_DB_PREFIX."liberty_content` + WHERE `content_id` = ? AND `content_type_guid` IN ('stockassembly','stockcomponent')", + [ $targetContentId ] + ); + if( $targetRow ) { + $assemblyHash = [ + 'content_id' => $gContent->mContentId, + 'item' => 'ASSEMBLY', + 'xref' => $targetContentId, + 'xkey' => (string)$kitCount, + 'xkey_ext' => $targetRow['title'], + 'edit' => $targetRow['content_type_guid'], + 'fAddXref' => 1, + ]; + $gContent->storeXref( $assemblyHash ); + if( $targetRow['content_type_guid'] === 'stockassembly' ) { + $gContent->explodeFromAssembly( $targetContentId, $kitCount ); + } else { + $nextXorder = (int)$gBitDb->getOne( + "SELECT COALESCE( MAX(x.`xorder`) + 1, 1 ) FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`content_id` = ? AND x.`item` IN ('SGL','PCK','SHT','VOL')", + [ $gContent->mContentId ] + ) ?: 1; + $qtyHash = [ + 'content_id' => $gContent->mContentId, + 'item' => 'SGL', + 'xref' => $targetContentId, + 'xkey' => (string)$kitCount, + 'xorder' => $nextXorder, + 'fAddXref' => 1, + ]; + $gContent->storeXref( $qtyHash ); + } + } + } + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); + die; + } elseif( !empty( $_REQUEST['delete'] ) ) { $gBitSystem->verifyPermission( 'p_stock_admin' ); if( !empty( $_REQUEST['cancel'] ) ) { @@ -133,10 +178,6 @@ if( $gContent->isValid() ) { $gBitSmarty->assign( 'gXrefInfo', $gContent->mXrefInfo ); } -// Pre-populate reference row for the form -$refRow = !empty( $gContent->mInfo['reference'] ) ? reset( $gContent->mInfo['reference'] ) : []; -$gBitSmarty->assign( 'refRow', $refRow ); - // Pre-format dates as dd/mm/yyyy for form fields $orderedDateVal = !empty( $gContent->mInfo['ref_start_date'] ) ? date( 'd/m/Y', strtotime( $gContent->mInfo['ref_start_date'] ) ) : ''; @@ -146,6 +187,31 @@ $gBitSmarty->assign( 'orderedDateVal', $orderedDateVal ); $gBitSmarty->assign( 'receivedDateVal', $receivedDateVal ); $gBitSmarty->assign( 'contactLookupUrl', CONTACT_PKG_URL.'includes/lookup_contact.php' ); +$isReqn = ( ( $gContent->mInfo['ref_type'] ?? '' ) === 'REQN' ); +if( $isReqn ) { + $assembly = new StockAssembly(); + $asmHash = [ 'show_empty' => true, 'sort_mode' => 'title_asc', 'max_records' => 1000 ]; + $assemblyList = $assembly->getList( $asmHash ); + $component = new StockComponent(); + $compHash = [ 'kitlocker_only' => true, 'sort_mode' => 'title_asc', 'max_records' => 1000 ]; + $componentList = $component->getList( $compHash ); + $itemList = array_merge( array_values( $assemblyList ), array_values( $componentList ) ); + usort( $itemList, fn( $a, $b ) => strcasecmp( $a['title'], $b['title'] ) ); + $itemIds = array_column( $itemList, 'content_id' ); + $klidMap = []; + if( $itemIds ) { + $klidRows = $gBitDb->getAll( + "SELECT x.`content_id`, x.`xkey` FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`item` = 'KLID' AND x.`content_id` IN (".implode( ',', array_fill( 0, count( $itemIds ), '?' ) ).")", + $itemIds + ); + foreach( $klidRows as $r ) { $klidMap[$r['content_id']] = $r['xkey']; } + } + $gBitSmarty->assign( 'itemListJson', json_encode( + array_map( fn( $i ) => [ 'id' => (int)$i['content_id'], 'text' => $i['title'], 'klid' => $klidMap[$i['content_id']] ?? '' ], $itemList ) + ) ); +} +$gBitSmarty->assign( 'isReqn', $isReqn ); $gBitSmarty->assign( 'refTypes', $refTypes ); $gBitSmarty->assign( 'errors', $gContent->mErrors ); diff --git a/import/ImportKitlockerAssemblies.php b/import/ImportKitlockerAssemblies.php index aa96a1e..9c12200 100644 --- a/import/ImportKitlockerAssemblies.php +++ b/import/ImportKitlockerAssemblies.php @@ -4,19 +4,47 @@ * * CSV column layout (0-based, header row skipped by loader): * 0 Title MERG product code / designation (used as lc.title) - * 1 KLID Kitlocker numeric ID code (stored as KLID xref on assemblies) + * 1 KLID Kitlocker numeric ID code (stored as KLID xref on both types) * 2 Description Long description (stored as content body) - * 3 KLSGL Kitlocker single-unit stock count (assemblies only → KLSGL xref) - * 4 KL3M 3-month sales count (assemblies only → KL3M xref) - * 5 Group Group number 1–28 → KLG01–KLG28 stgrp xref (both types) + * 3 KLSGL Kitlocker single-unit stock count (stored as KLSGL xref on both types) + * 4 KL3M 3-month sales count (stored as KL3M xref on both types) + * 5 Group Group number 1–99 → KLG01–KLG99 stgrp xref (both types) * 6 Type 'A' = StockAssembly, 'C' = StockComponent * + * On reload, existing records are not re-created but their kitlocker xrefs are upserted, + * so a fresh import run will tag components that were previously missing KLID etc. + * * @package stock */ use Bitweaver\Stock\StockAssembly; use Bitweaver\Stock\StockComponent; +function stockKitlockerXrefUpsert( int $contentId, string $item, string $value ): void { + global $gBitDb; + $existingId = $gBitDb->getOne( + "SELECT `xref_id` FROM `".BIT_DB_PREFIX."liberty_xref` + WHERE `content_id` = ? AND `item` = ?", + [ $contentId, $item ] + ); + if( $existingId ) { + $gBitDb->associateUpdate( + BIT_DB_PREFIX.'liberty_xref', + [ 'xkey' => substr( $value, 0, 32 ), 'last_update_date' => $gBitDb->NOW() ], + [ 'xref_id' => $existingId ] + ); + } else { + $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [ + 'xref_id' => $gBitDb->GenID( 'liberty_xref_seq' ), + 'content_id' => $contentId, + 'item' => $item, + 'xkey' => substr( $value, 0, 32 ), + 'xorder' => 0, + 'last_update_date' => $gBitDb->NOW(), + ] ); + } +} + function stockExpungeKitlockerItemByTitle( string $title, string $type ): bool { global $gBitDb; @@ -53,65 +81,43 @@ function stockImportKitlockerItem( array $data, int $rowNum ): array { return $result; } - $guid = ( $type === 'A' ) ? 'stockassembly' : 'stockcomponent'; - - $exists = $gBitDb->getOne( - "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc - WHERE lc.`content_type_guid` = ? AND lc.`title` = ?", - [ $guid, $title ] - ); - if( $exists ) { - $result['skipped']++; - $result['errors'][] = "Row $rowNum: '$title' already exists, skipped."; - return $result; - } - + $guid = ( $type === 'A' ) ? 'stockassembly' : 'stockcomponent'; $klid = trim( $data[1] ?? '' ); $desc = trim( $data[2] ?? '' ); $klsgl = trim( $data[3] ?? '' ); $kl3m = trim( $data[4] ?? '' ); $group = (int)( $data[5] ?? 0 ); - $obj = ( $type === 'A' ) ? new StockAssembly() : new StockComponent(); - $pHash = [ - 'title' => $title, - 'edit' => $desc, - 'format_guid' => 'bithtml', - ]; - if( !$obj->store( $pHash ) ) { - $result['skipped']++; - $result['errors'][] = "Row $rowNum: failed to store '$title'."; - return $result; - } + $contentId = (int)$gBitDb->getOne( + "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc + WHERE lc.`content_type_guid` = ? AND lc.`title` = ?", + [ $guid, $title ] + ); - $contentId = $obj->mContentId; + if( !$contentId ) { + $obj = ( $type === 'A' ) ? new StockAssembly() : new StockComponent(); + $pHash = [ + 'title' => $title, + 'edit' => $desc, + 'format_guid' => 'bithtml', + ]; + if( !$obj->store( $pHash ) ) { + $result['skipped']++; + $result['errors'][] = "Row $rowNum: failed to store '$title'."; + return $result; + } + $contentId = $obj->mContentId; + } - // Group tag — shared across both types via 'stock' package-level stgrp + // Group tag — both types if( $group >= 1 && $group <= 99 ) { - $xrefId = $gBitDb->GenID( 'liberty_xref_seq' ); - $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [ - 'xref_id' => $xrefId, - 'content_id' => $contentId, - 'item' => sprintf( 'KLG%02d', $group ), - 'xorder' => 0, - 'last_update_date' => $gBitDb->NOW(), - ] ); + stockKitlockerXrefUpsert( $contentId, sprintf( 'KLG%02d', $group ), '' ); } - // Assembly-specific kitlocker xrefs - if( $type === 'A' ) { - foreach( [ 'KLID' => $klid, 'KLSGL' => $klsgl, 'KL3M' => $kl3m ] as $item => $value ) { - if( $value !== '' ) { - $xrefId = $gBitDb->GenID( 'liberty_xref_seq' ); - $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [ - 'xref_id' => $xrefId, - 'content_id' => $contentId, - 'item' => $item, - 'xorder' => 0, - 'xkey' => substr( $value, 0, 32 ), - 'last_update_date' => $gBitDb->NOW(), - ] ); - } + // Kitlocker xrefs — same set for assemblies and components + foreach( [ 'KLID' => $klid, 'KLSGL' => $klsgl, 'KL3M' => $kl3m ] as $item => $value ) { + if( $value !== '' ) { + stockKitlockerXrefUpsert( $contentId, $item, $value ); } } diff --git a/includes/classes/StockComponent.php b/includes/classes/StockComponent.php index 6a67318..37d3659 100755 --- a/includes/classes/StockComponent.php +++ b/includes/classes/StockComponent.php @@ -189,6 +189,10 @@ class StockComponent extends StockBase { $bindVars[] = $term; } + if( !empty( $pListHash['kitlocker_only'] ) ) { + $whereSql .= " AND EXISTS (SELECT 1 FROM `".BIT_DB_PREFIX."liberty_xref` kx WHERE kx.`content_id` = lc.`content_id` AND kx.`item` = 'KLID')"; + } + $this->getServicesSql( 'content_list_sql_function', $selectSql, $joinSql, $whereSql, $bindVars ); $orderby = !empty( $pListHash['sort_mode'] ) diff --git a/includes/classes/StockMovement.php b/includes/classes/StockMovement.php index 543c588..d731050 100644 --- a/includes/classes/StockMovement.php +++ b/includes/classes/StockMovement.php @@ -194,6 +194,23 @@ class StockMovement extends LibertyContent { } } unset( $row ); + + $assemblyGroup = $this->mXrefInfo->mGroups['assembly'] ?? null; + if( !$assemblyGroup || empty( $assemblyGroup->mXrefs ) ) return; + $assemblyIds = array_values( array_unique( array_filter( array_column( $assemblyGroup->mXrefs, 'xref' ) ) ) ); + if( !$assemblyIds ) return; + $assemblies = $this->mDb->getAssoc( + "SELECT lc.`content_id`, lc.`title`, lc.`data` FROM `".BIT_DB_PREFIX."liberty_content` lc + WHERE lc.`content_id` IN (".implode( ',', array_fill( 0, count( $assemblyIds ), '?' ) ).")", + $assemblyIds + ); + foreach( $assemblyGroup->mXrefs as &$row ) { + if( !empty( $row['xref'] ) && isset( $assemblies[$row['xref']] ) ) { + $row['linked_title'] = $assemblies[$row['xref']]['title']; + $row['linked_desc'] = $assemblies[$row['xref']]['data']; + } + } + unset( $row ); } /** diff --git a/list_stock.php b/list_stock.php index 1a8e7c7..141b187 100644 --- a/list_stock.php +++ b/list_stock.php @@ -134,8 +134,24 @@ if( $assemblyContentId && isset( $assemblyList[$assemblyContentId] ) ) { $assemblyTitle = $assemblyList[$assemblyContentId]['title']; } +// KLID map for assembly autocomplete +$asmIds = array_keys( $assemblyList ); +$asmKlidMap = []; +if( $asmIds ) { + $klidRows = $gBitDb->getAll( + "SELECT x.`content_id`, x.`xkey` FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`item` = 'KLID' AND x.`content_id` IN (".implode( ',', array_fill( 0, count( $asmIds ), '?' ) ).")", + $asmIds + ); + foreach( $klidRows as $r ) { $asmKlidMap[$r['content_id']] = $r['xkey']; } +} +$assemblyListJson = json_encode( array_values( array_map( + fn( $i ) => [ 'id' => (int)$i['content_id'], 'text' => $i['title'], 'klid' => $asmKlidMap[$i['content_id']] ?? '' ], + $assemblyList +) ) ); + $gBitSmarty->assign( 'stockList', $stockList ); -$gBitSmarty->assign( 'assemblyList', $assemblyList ); +$gBitSmarty->assign( 'assemblyListJson', $assemblyListJson ); $gBitSmarty->assign( 'assemblyContentId', $assemblyContentId ); $gBitSmarty->assign( 'assemblyTitle', $assemblyTitle ); $gBitSmarty->assign( 'find', $find ); diff --git a/templates/add_movement_component.tpl b/templates/add_movement_component.tpl new file mode 100644 index 0000000..7eb1175 --- /dev/null +++ b/templates/add_movement_component.tpl @@ -0,0 +1,113 @@ +{strip} +<div class="edit stock"> + <div class="header"> + <h1>{tr}Add Component{/tr}: {$gContent->getTitle()|escape}</h1> + </div> + + <div class="body"> + {formfeedback error=$errors} + + {form id="addComponentForm" ipackage="stock" ifile="add_movement_component.php"} + <input type="hidden" name="content_id" value="{$gContent->mContentId}" /> + + <div class="form-group"> + {formlabel label="Component" for="component_title" mandatory="y"} + {forminput} + <input type="hidden" id="component_id_unused" /> + <div style="position:relative"> + <input type="text" class="form-control" name="component_title" id="component_title" + autocomplete="off" + value="{$smarty.request.component_title|default:''|escape}" + placeholder="{tr}Type to search…{/tr}" /> + <ul id="comp_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> + {formhelp note="Type to search existing components, or enter a new title to create one."} + {/forminput} + </div> + + <div class="form-group"> + {formlabel label="Qty Type" for="item"} + {forminput} + <select name="item" id="item" class="form-control"> + {foreach from=$validItems key=code item=label} + <option value="{$code}"{if $smarty.request.item|default:'SGL' eq $code} selected="selected"{/if}>{$label|escape}</option> + {/foreach} + </select> + {/forminput} + </div> + + <div class="form-group"> + {formlabel label="Qty" for="xkey"} + {forminput} + <input type="text" class="form-control" name="xkey" id="xkey" + value="{$smarty.request.xkey|default:''|escape}" /> + {/forminput} + </div> + + <div class="form-group"> + {formlabel label="Ref designators" for="xkey_ext"} + {forminput} + <input type="text" class="form-control" name="xkey_ext" id="xkey_ext" + value="{$smarty.request.xkey_ext|default:''|escape}" /> + {/forminput} + </div> + + <div class="form-group"> + {formlabel label="Note" for="edit"} + {forminput} + <input type="text" class="form-control" name="edit" id="edit" + value="{$smarty.request.edit|default:''|escape}" /> + {/forminput} + </div> + + <div class="form-group submit"> + <input type="submit" class="btn btn-default" name="fCancel" value="{tr}Cancel{/tr}" /> + <input type="submit" class="btn btn-primary" name="fAddComponent" value="{tr}Add Component{/tr}" /> + </div> + {/form} + </div> +</div> +{/strip} +<script> +(function($) { + var timer; + var $input = $('#component_title'); + var $dd = $('#comp_dropdown'); + + $input.on('input', function() { + var q = $(this).val(); + clearTimeout(timer); + $dd.hide().empty(); + if (q.length < 2) return; + timer = setTimeout(function() { + $.getJSON('{$lookupUrl}', {ldelim}q: q{rdelim}, function(data) { + if (!data.length) return; + $.each(data, function(i, row) { + $dd.append($('<li>').append( + $('<a>').attr('href','#').data('title', row.title).text(row.title) + )); + }); + $dd.show(); + }); + }, 250); + }); + + $(document).on('mousedown', '#comp_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('title')); + $dd.hide().empty(); + }); + + $input.on('blur', function() { setTimeout(function() { $dd.hide(); }, 150); }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $links = $dd.find('a'), idx = $links.index($dd.find('li.active a')); + if (e.key === 'ArrowDown') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx + 1 < $links.length ? idx + 1 : 0).parent().addClass('active'); } + else if (e.key === 'ArrowUp') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx > 0 ? idx - 1 : $links.length - 1).parent().addClass('active'); } + else if (e.key === 'Enter') { var $a = $dd.find('li.active a'); if ($a.length) { e.preventDefault(); $a.trigger('mousedown'); } } + else if (e.key === 'Escape') { $dd.hide(); } + }); +}(jQuery)); +</script> diff --git a/templates/add_requisition.tpl b/templates/add_requisition.tpl index 58796e1..3b6051b 100644 --- a/templates/add_requisition.tpl +++ b/templates/add_requisition.tpl @@ -18,27 +18,36 @@ </div> <div class="form-group"> - {formlabel label="Assembly" for="assembly_content_id" mandatory="y"} + {formlabel label="Ordered" for="ordered_date"} {forminput} - <select name="assembly_content_id" id="assembly_content_id" class="form-control"> - <option value="">{tr}— Select assembly —{/tr}</option> - {foreach from=$assemblyList item=asm} - <option value="{$asm.content_id|escape}" - {if $preselect == $asm.content_id} selected="selected"{/if}> - {$asm.title|escape} - </option> - {/foreach} - </select> + <input type="text" class="form-control input-small" name="ordered_date" id="ordered_date" + placeholder="dd/mm/yyyy" maxlength="10" + value="{$smarty.request.ordered_date|default:$todayFormatted|escape}" /> {/forminput} </div> <div class="form-group"> - {formlabel label="Kits" for="kit_count"} + {formlabel label="Item" for="assembly_search" mandatory="y"} + {forminput} + <input type="hidden" name="assembly_content_id" id="assembly_content_id" + value="{$preselect|default:''|escape}" /> + <div style="position:relative"> + <input type="text" class="form-control" id="assembly_search" + autocomplete="off" + value="{$preselectTitle|escape}" + placeholder="Type to search…" /> + <ul id="assembly_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> + {/forminput} + </div> + + <div class="form-group"> + {formlabel label="Qty" for="kit_count"} {forminput} <input type="number" class="form-control input-sm" name="kit_count" id="kit_count" min="1" step="1" style="width:6em" value="{$kitCount|escape}" /> - {formhelp note="Number of assemblies to requisition"} {/forminput} </div> @@ -51,3 +60,60 @@ </div> </div> {/strip} +<script> +(function($) { + var items = {$itemListJson}; + var $input = $('#assembly_search'); + var $hidden = $('#assembly_content_id'); + var $dd = $('#assembly_dropdown'); + + $input.on('input', function() { + var q = this.value.toLowerCase().trim(); + $dd.hide().empty(); + $hidden.val(''); + if (!q) return; + var matched = items.filter(function(i) { + return i.text.toLowerCase().indexOf(q) !== -1 || (i.klid && i.klid.toLowerCase().indexOf(q) !== -1); + }); + if (!matched.length) return; + matched.forEach(function(i) { + var label = i.text + (i.klid ? ' (' + i.klid + ')' : ''); + $dd.append($('<li>').append( + $('<a>').attr('href', '#').data('id', i.id).data('text', i.text).text(label) + )); + }); + $dd.show(); + }); + + $(document).on('mousedown', '#assembly_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('text')); + $hidden.val($(this).data('id')); + $dd.hide().empty(); + }); + + $input.on('blur', function() { + setTimeout(function() { $dd.hide(); }, 150); + }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $links = $dd.find('a'); + var idx = $links.index($dd.find('li.active a')); + if (e.key === 'ArrowDown') { + e.preventDefault(); + $links.parent().removeClass('active'); + $links.eq(idx + 1 < $links.length ? idx + 1 : 0).parent().addClass('active'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + $links.parent().removeClass('active'); + $links.eq(idx > 0 ? idx - 1 : $links.length - 1).parent().addClass('active'); + } else if (e.key === 'Enter') { + var $active = $dd.find('li.active a'); + if ($active.length) { e.preventDefault(); $active.trigger('mousedown'); } + } else if (e.key === 'Escape') { + $dd.hide(); + } + }); +}(jQuery)); +</script> diff --git a/templates/add_supplier.tpl b/templates/add_supplier.tpl index 72e0195..adcfd52 100644 --- a/templates/add_supplier.tpl +++ b/templates/add_supplier.tpl @@ -11,14 +11,15 @@ <input type="hidden" name="content_id" value="{$gContent->mContentId}" /> <div class="form-group"> - {formlabel label="Supplier" for="supplier_content_id"} + {formlabel label="Supplier" for="sup_search"} {forminput} - <select name="supplier_content_id" id="supplier_content_id" class="form-control"> - <option value="">{tr}-- Select supplier --{/tr}</option> - {foreach from=$supplierList item=sup} - <option value="{$sup.content_id|escape}">{$sup.title|escape}</option> - {/foreach} - </select> + <input type="hidden" name="supplier_content_id" id="supplier_content_id" value="" /> + <div style="position:relative"> + <input type="text" class="form-control" id="sup_search" + autocomplete="off" placeholder="{tr}Type to search contacts…{/tr}" /> + <ul id="sup_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> {/forminput} </div> @@ -48,6 +49,54 @@ <input type="submit" class="btn btn-primary" name="fAddSupplier" value="{tr}Save{/tr}" /> </div> {/form} - </div><!-- end .body --> -</div><!-- end .stock --> + </div> +</div> {/strip} +<script> +(function($) { + var timer, contacts = []; + var $input = $('#sup_search'); + var $hidden = $('#supplier_content_id'); + var $dd = $('#sup_dropdown'); + + $input.on('input', function() { + var q = $(this).val(); + clearTimeout(timer); + $dd.hide().empty(); + contacts = []; + if (q.length < 2) return; + timer = setTimeout(function() { + $.getJSON('{$contactLookupUrl}', {ldelim}q: q{rdelim}, function(data) { + contacts = data; + if (!data.length) return; + $.each(data, function(i, row) { + var label = row.title + (row.scref ? ' (' + row.scref + ')' : ''); + $dd.append($('<li>').append( + $('<a>').attr('href','#').data('id', row.content_id).data('label', label).text(label) + )); + }); + $dd.show(); + }); + }, 250); + }); + + $(document).on('mousedown', '#sup_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('label')); + $hidden.val($(this).data('id')); + $dd.hide().empty(); + contacts = []; + }); + + $input.on('blur', function() { setTimeout(function() { $dd.hide(); }, 150); }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $links = $dd.find('a'), idx = $links.index($dd.find('li.active a')); + if (e.key === 'ArrowDown') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx + 1 < $links.length ? idx + 1 : 0).parent().addClass('active'); } + else if (e.key === 'ArrowUp') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx > 0 ? idx - 1 : $links.length - 1).parent().addClass('active'); } + else if (e.key === 'Enter') { var $a = $dd.find('li.active a'); if ($a.length) { e.preventDefault(); $a.trigger('mousedown'); } } + else if (e.key === 'Escape') { $dd.hide(); } + }); +}(jQuery)); +</script> diff --git a/templates/edit_movement.tpl b/templates/edit_movement.tpl index baec74b..c4081dc 100644 --- a/templates/edit_movement.tpl +++ b/templates/edit_movement.tpl @@ -24,6 +24,7 @@ {/forminput} </div> + {if !$isReqn} {if $refTypes} <div class="form-group"> {formlabel label="Movement Type" mandatory="y"} @@ -31,8 +32,7 @@ {foreach from=$refTypes key=item item=label} <label class="radio-inline"> <input type="radio" name="movement_type" value="{$item|escape}" - {if $refRow.item eq $item} checked="checked" - {elseif !$refRow && $smarty.foreach.default.first} checked="checked"{/if} /> {$label|escape} + {if ($gContent->mInfo.ref_type|default:'TRANS') eq $item} checked="checked"{/if} /> {$label|escape} </label> {/foreach} {/forminput} @@ -44,11 +44,14 @@ {forminput} <input type="hidden" name="ref_contact_id" id="ref_contact_id" value="{$gContent->mInfo.ref_contact_id|default:''|escape}" /> - <input type="text" class="form-control" name="ref_from" id="ref_from" - autocomplete="off" list="contact_suggestions" - value="{if $gContent->mInfo.ref_contact_name}{$gContent->mInfo.ref_contact_name|escape}{else}{$refRow.data|default:''|escape}{/if}" - maxlength="160" placeholder="Type to search contacts…" /> - <datalist id="contact_suggestions"></datalist> + <div style="position:relative"> + <input type="text" class="form-control" name="ref_from" id="ref_from" + autocomplete="off" + value="{if $gContent->mInfo.ref_contact_name}{$gContent->mInfo.ref_contact_name|escape}{else}{$gContent->mInfo.ref_from_data|default:''|escape}{/if}" + maxlength="160" placeholder="Type to search contacts…" /> + <ul id="contact_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> {/forminput} </div> @@ -56,9 +59,10 @@ {formlabel label="Ref Key" for="ref_key"} {forminput} <input type="text" class="form-control" name="ref_key" id="ref_key" - value="{$refRow.xkey|default:''|escape}" maxlength="160" /> + value="{$gContent->mInfo.ref_key|default:''|escape}" maxlength="160" /> {/forminput} </div> + {/if} <div class="form-group"> {formlabel label="Ordered" for="ordered_date"} @@ -103,7 +107,21 @@ {if $gContent->isValid()} - {* ── Upload CSV ── *} + {if $gXrefInfo->mGroups} + {jstabs} + {foreach $gXrefInfo->mGroups as $xrefGroup} + {if $xrefGroup->mXGroup neq 'reference' && ($xrefGroup->mXGroup neq 'assembly' || $isReqn)} + {include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate) + xrefGroup=$xrefGroup + allow_add=true + allow_edit=true} + {/if} + {/foreach} + {/jstabs} + {/if} + + {if !$isReqn} + {* ── Upload CSV (orders/transfers only) ── *} <h4>{tr}Upload CSV{/tr}</h4> {form enctype="multipart/form-data" ipackage="stock" ifile="edit_movement.php"} <input type="hidden" name="content_id" value="{$gContent->mContentId|escape}" /> @@ -112,6 +130,7 @@ <input type="submit" class="btn btn-default" name="upload_csv" value="{tr}Upload{/tr}" /> </div> {/form} + {/if} {* ── Upload results ── *} {if isset($csvLoaded)} @@ -126,20 +145,6 @@ {/if} {/if} - {* ── Xref tabs — quantity BOM only; reference handled above in form ── *} - {if $gXrefInfo->mGroups} - {jstabs} - {foreach $gXrefInfo->mGroups as $xrefGroup} - {if $xrefGroup->mXGroup neq 'reference'} - {include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate) - xrefGroup=$xrefGroup - allow_add=true - allow_edit=true} - {/if} - {/foreach} - {/jstabs} - {/if} - {/if} </div><!-- end .body --> @@ -147,27 +152,61 @@ <script> (function($) { var timer, contacts = []; - $('#ref_from').on('input', function() { + var $input = $('#ref_from'); + var $hidden = $('#ref_contact_id'); + var $dd = $('#contact_dropdown'); + + $input.on('input', function() { var q = $(this).val(); clearTimeout(timer); - if (q.length < 2) { $('#contact_suggestions').empty(); contacts = []; return; } + $dd.hide().empty(); + contacts = []; + if (q.length < 2) return; timer = setTimeout(function() { $.getJSON('{$contactLookupUrl}', {ldelim}q: q{rdelim}, function(data) { contacts = data; - var dl = $('#contact_suggestions').empty(); + if (!data.length) return; $.each(data, function(i, row) { var label = row.title + (row.scref ? ' (' + row.scref + ')' : ''); - dl.append($('<option>').val(label).attr('data-id', row.content_id)); + $dd.append($('<li>').append( + $('<a>').attr('href','#').data('id', row.content_id).data('label', label).text(label) + )); }); + $dd.show(); }); }, 250); - }).on('change', function() { - var val = $(this).val(), found = null; - $.each(contacts, function(i, row) { - var label = row.title + (row.scref ? ' (' + row.scref + ')' : ''); - if (label === val || row.title === val || row.scref === val) { found = row; return false; } - }); - $('#ref_contact_id').val(found ? found.content_id : ''); + }); + + $(document).on('mousedown', '#contact_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('label')); + $hidden.val($(this).data('id')); + $dd.hide().empty(); + contacts = []; + }); + + $input.on('blur', function() { + setTimeout(function() { $dd.hide(); }, 150); + }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $items = $dd.find('a'); + var idx = $dd.find('li.active a').length ? $items.index($dd.find('li.active a')) : -1; + if (e.key === 'ArrowDown') { + e.preventDefault(); + $items.parent().removeClass('active'); + $items.eq(idx + 1 < $items.length ? idx + 1 : 0).parent().addClass('active'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + $items.parent().removeClass('active'); + $items.eq(idx > 0 ? idx - 1 : $items.length - 1).parent().addClass('active'); + } else if (e.key === 'Enter') { + var $active = $dd.find('li.active a'); + if ($active.length) { e.preventDefault(); $active.trigger('mousedown'); } + } else if (e.key === 'Escape') { + $dd.hide(); + } }); }(jQuery)); </script> diff --git a/templates/list_movements.tpl b/templates/list_movements.tpl index 220eca1..aef033b 100644 --- a/templates/list_movements.tpl +++ b/templates/list_movements.tpl @@ -3,6 +3,7 @@ <header> <div class="floaticon"> {if $gBitUser->hasPermission('p_stock_create')} + <a href="{$smarty.const.STOCK_PKG_URL}add_requisition.php">{biticon ipackage="icons" iname="list-add" iexplain="Add Requisition"}</a> <a href="{$smarty.const.STOCK_PKG_URL}edit_movement.php">{biticon ipackage="icons" iname="view-task-add" iexplain="Add Movement"}</a> {/if} </div> diff --git a/templates/list_stock.tpl b/templates/list_stock.tpl index efbe222..edb6afe 100644 --- a/templates/list_stock.tpl +++ b/templates/list_stock.tpl @@ -12,15 +12,14 @@ {form ipackage="stock" ifile="list_stock.php" method="get"} <div class="form-inline" style="margin-bottom:1em"> <div class="form-group"> - <select name="assembly_content_id" class="form-control input-sm"> - <option value="">{tr}All components{/tr}</option> - {foreach from=$assemblyList item=asm} - <option value="{$asm.content_id|escape}" - {if $assemblyContentId == $asm.content_id} selected="selected"{/if}> - {$asm.title|escape} - </option> - {/foreach} - </select> + <input type="hidden" name="assembly_content_id" id="ls_asm_id" value="{$assemblyContentId|default:''|escape}" /> + <div style="position:relative;display:inline-block;vertical-align:top"> + <input type="text" class="form-control input-sm" id="ls_asm_search" + autocomplete="off" placeholder="{tr}All components{/tr}" + value="{$assemblyTitle|escape}" /> + <ul id="ls_asm_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> </div> <div class="form-group"> <input type="text" class="form-control input-sm" name="find" @@ -99,3 +98,45 @@ </section> </div> {/strip} +<script> +(function($) { + var items = {$assemblyListJson}; + var $input = $('#ls_asm_search'); + var $hidden = $('#ls_asm_id'); + var $dd = $('#ls_asm_dropdown'); + + $input.on('input', function() { + var q = this.value.toLowerCase().trim(); + $hidden.val(''); + $dd.hide().empty(); + if (!q) return; + var matched = items.filter(function(i) { + return i.text.toLowerCase().indexOf(q) !== -1 || (i.klid && i.klid.toLowerCase().indexOf(q) !== -1); + }); + if (!matched.length) return; + matched.forEach(function(i) { + var label = i.text + (i.klid ? ' (' + i.klid + ')' : ''); + $dd.append($('<li>').append($('<a>').attr('href','#').data('id', i.id).data('text', i.text).text(label))); + }); + $dd.show(); + }); + + $(document).on('mousedown', '#ls_asm_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('text')); + $hidden.val($(this).data('id')); + $dd.hide().empty(); + }); + + $input.on('blur', function() { setTimeout(function() { $dd.hide(); }, 150); }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $links = $dd.find('a'), idx = $links.index($dd.find('li.active a')); + if (e.key === 'ArrowDown') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx + 1 < $links.length ? idx + 1 : 0).parent().addClass('active'); } + else if (e.key === 'ArrowUp') { e.preventDefault(); $links.parent().removeClass('active'); $links.eq(idx > 0 ? idx - 1 : $links.length - 1).parent().addClass('active'); } + else if (e.key === 'Enter') { var $a = $dd.find('li.active a'); if ($a.length) { e.preventDefault(); $a.trigger('mousedown'); } } + else if (e.key === 'Escape') { $dd.hide(); } + }); +}(jQuery)); +</script> diff --git a/templates/stockmovement/view_xref_bom_group.tpl b/templates/stockmovement/view_xref_bom_group.tpl index d253e92..0787905 100644 --- a/templates/stockmovement/view_xref_bom_group.tpl +++ b/templates/stockmovement/view_xref_bom_group.tpl @@ -28,7 +28,7 @@ </div> {if $allow_add && $gContent->isValid() && $gContent->hasUpdatePermission() && !$isHistory} <div> - {smartlink ititle="Add record" ipackage="liberty" ifile="add_xref.php" biticon="list-add" content_id=$gContent->mInfo.content_id group=$xrefGroup->mSortOrder} + {smartlink ititle="Add component" ipackage="stock" ifile="add_movement_component.php" biticon="list-add" content_id=$gContent->mInfo.content_id} </div> {/if} {/legend} diff --git a/templates/view_movement.tpl b/templates/view_movement.tpl index f46ca6c..f3d157f 100644 --- a/templates/view_movement.tpl +++ b/templates/view_movement.tpl @@ -45,7 +45,7 @@ {if $gXrefInfo->mGroups} {jstabs} {foreach $gXrefInfo->mGroups as $xrefGroup} - {if $xrefGroup->mXGroup neq 'reference'} + {if $xrefGroup->mXGroup neq 'reference' && ($xrefGroup->mXGroup neq 'assembly' || $isReqn)} {include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate) xrefGroup=$xrefGroup allow_add=false diff --git a/templates/view_xref_assembly_group.tpl b/templates/view_xref_assembly_group.tpl new file mode 100644 index 0000000..ab77013 --- /dev/null +++ b/templates/view_xref_assembly_group.tpl @@ -0,0 +1,113 @@ +{assign var=xrefAllowEdit value=$allow_edit|default:false} +{assign var=xrefAllowAdd value=$allow_add|default:false} +{assign var=isHistory value=($xrefGroup->mXGroup eq 'history')} +{jstab title="{tr}Assembly{/tr} ({$xrefGroup->mXrefs|@count})"} +{legend legend="Assembly"} +<div class="form-group table-responsive"> + <table class="table"> + <thead> + <tr> + <th>{tr}Item{/tr}</th> + <th>{tr}Type{/tr}</th> + <th>{tr}Qty{/tr}</th> + {if $xrefAllowEdit}<th></th>{/if} + </tr> + </thead> + <tbody> + {if $xrefGroup->mXrefs} + {foreach $xrefGroup->mXrefs as $xrefInfo} + <tr class="{cycle values='even,odd'}"> + {include file=$gContent->getXrefRecordTemplate($xrefInfo.template)} + </tr> + {/foreach} + {else} + <tr class="norecords"> + <td colspan="{if $xrefAllowEdit}4{else}3{/if}">{tr}No items{/tr}</td> + </tr> + {/if} + </tbody> + </table> +</div> + +{if $xrefAllowAdd && $itemListJson} +<hr /> +<h5>{tr}Add item{/tr}</h5> +{form ipackage="stock" ifile="edit_movement.php"} + <input type="hidden" name="content_id" value="{$gContent->mContentId|escape}" /> + <div class="form-inline"> + <input type="hidden" name="assembly_content_id" id="asm_add_id" value="" /> + <div style="position:relative;display:inline-block;width:20em;vertical-align:top"> + <input type="text" class="form-control" id="asm_add_search" + autocomplete="off" placeholder="{tr}Type to search…{/tr}" /> + <ul id="asm_add_dropdown" class="dropdown-menu" + style="display:none;position:absolute;width:100%;z-index:1000;max-height:220px;overflow-y:auto"></ul> + </div> + + <input type="number" class="form-control input-sm" name="kit_count" + min="1" step="1" value="1" style="width:5em;display:inline-block" title="{tr}Qty / kits{/tr}" /> + + <input type="submit" class="btn btn-default btn-sm" name="fAddAssembly" value="{tr}Add{/tr}" /> + </div> +{/form} +{/if} +{/legend} +{/jstab} +{if $xrefAllowAdd && $itemListJson} +<script> +(function($) { + var items = {$itemListJson}; + var $input = $('#asm_add_search'); + var $hidden = $('#asm_add_id'); + var $dd = $('#asm_add_dropdown'); + + $input.on('input', function() { + var q = this.value.toLowerCase().trim(); + $dd.hide().empty(); + $hidden.val(''); + if (!q) return; + var matched = items.filter(function(i) { + return i.text.toLowerCase().indexOf(q) !== -1 || (i.klid && i.klid.toLowerCase().indexOf(q) !== -1); + }); + if (!matched.length) return; + matched.forEach(function(i) { + var label = i.text + (i.klid ? ' (' + i.klid + ')' : ''); + $dd.append($('<li>').append( + $('<a>').attr('href','#').data('id', i.id).data('text', i.text).text(label) + )); + }); + $dd.show(); + }); + + $(document).on('mousedown', '#asm_add_dropdown a', function(e) { + e.preventDefault(); + $input.val($(this).data('text')); + $hidden.val($(this).data('id')); + $dd.hide().empty(); + }); + + $input.on('blur', function() { + setTimeout(function() { $dd.hide(); }, 150); + }); + + $input.on('keydown', function(e) { + if (!$dd.is(':visible')) return; + var $links = $dd.find('a'); + var idx = $links.index($dd.find('li.active a')); + if (e.key === 'ArrowDown') { + e.preventDefault(); + $links.parent().removeClass('active'); + $links.eq(idx + 1 < $links.length ? idx + 1 : 0).parent().addClass('active'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + $links.parent().removeClass('active'); + $links.eq(idx > 0 ? idx - 1 : $links.length - 1).parent().addClass('active'); + } else if (e.key === 'Enter') { + var $active = $dd.find('li.active a'); + if ($active.length) { e.preventDefault(); $active.trigger('mousedown'); } + } else if (e.key === 'Escape') { + $dd.hide(); + } + }); +}(jQuery)); +</script> +{/if} diff --git a/templates/view_xref_assembly_item.tpl b/templates/view_xref_assembly_item.tpl new file mode 100644 index 0000000..e1572d6 --- /dev/null +++ b/templates/view_xref_assembly_item.tpl @@ -0,0 +1,24 @@ +{strip} +<td> + {assign var=asmTitle value=$xrefInfo.linked_title|default:$xrefInfo.xkey_ext} + {if $xrefInfo.xref} + <a href="{if $xrefInfo.data eq 'stockassembly'}{$smarty.const.STOCK_PKG_URL}view_assembly.php?content_id={$xrefInfo.xref}{else}{$smarty.const.STOCK_PKG_URL}view_component.php?content_id={$xrefInfo.xref}{/if}">{$asmTitle|escape}</a> + {else} + {$asmTitle|escape} + {/if} + {if $xrefInfo.linked_desc} + <div class="small text-muted">{$xrefInfo.linked_desc|strip_tags|truncate:120|escape}</div> + {/if} +</td> +<td>{if $xrefInfo.data eq 'stockassembly'}{tr}Assembly{/tr}{else}{tr}Component{/tr}{/if}</td> +<td>{$xrefInfo.xkey|escape}</td> +{if $xrefAllowEdit|default:false} +<td> + <span class="actionicon"> + {if $gContent->hasExpungePermission()} + {smartlink ititle="Remove" ipackage="liberty" ifile="edit_xref.php" biticon="user-trash" content_id=$gContent->mInfo.content_id xref_id=$xrefInfo.xref_id expunge=1} + {/if} + </span> +</td> +{/if} +{/strip} diff --git a/view_movement.php b/view_movement.php index 43327ae..395fce0 100644 --- a/view_movement.php +++ b/view_movement.php @@ -23,5 +23,6 @@ $gBitSystem->setCanonicalLink( $gContent->getDisplayUrl() ); $gContent->loadXrefInfo(); $gBitSmarty->assign( 'gXrefInfo', $gContent->mXrefInfo ); +$gBitSmarty->assign( 'isReqn', ( ( $gContent->mInfo['ref_type'] ?? '' ) === 'REQN' ) ); $gBitSystem->display( 'bitpackage:stock/view_movement.tpl', $gContent->getTitle() ); |
