Adding "Print Stock Need" to home dashboard
📁 Complete Implementation Tree
ultimate-pos/
├── app/
│ ├── Http/
│ │ └── Controllers/
│ │ └── HomeController.php # ✅ Update getProductStockAlert method
│ └── Utils/
│ └── ProductUtil.php # ✅ Update getProductAlert method
├── resources/
│ └── views/
│ └── home/
│ └── index.blade.php # ✅ Add print button + JavaScript
├── lang/
│ └── en/
│ └── lang_v1.php # ✅ Add new translations
├── public/
│ ├── js/
│ │ └── home.js # ⚪ Add location filter event (optional)
│ └── uploads/
│ └── business_logos/
│ └── company-logo.png # 🖼️ Company logo file
└──
📦 Download Starter Files
Download Implementation Template
Password: ultimatepos_psn_2025
This zip file contains the complete folder structure with placeholder files to help you get started quickly. Extract it to see the exact organization needed for implementation.
🔧 File Modifications Summary
File | Changes Required | Status |
---|---|---|
resources/views/home/index.blade.php | Add print button + JavaScript | ✅ Required |
app/Http/Controllers/HomeController.php | Update getProductStockAlert() method | ✅ Required |
app/Utils/ProductUtil.php | Update getProductAlert() method | ✅ Required |
lang/en/lang_v1.php | Add new translations | ✅ Required |
public/js/home.js | Add location filter event | ⚪ Optional |
Screenshots and Results
1. Ultimate POS Interface with Print Button
The Ultimate POS dashboard showing the Product Stock Alert section with the new "Print Stock Need" button. The table displays all 6 columns correctly: Product, Location, Current Stock, Alert Quantity, Expected Stock, and Stock Need.
2. Professional Print Preview
The generated print report showing professional formatting with:
- Company branding (ERP)
- Report title and generation timestamp
- Summary statistics (Total Products: 1, Critical Items: 1, Total Current Stock: 8.00, etc.)
- Formatted data table with color-coded Stock Need column
- Footer with print details
3. Browser Print Dialog
Browser print dialog showing "Save as PDF" option with the print preview visible in the background. Users can save the report as PDF or print directly to a physical printer.
Implementation Success
These screenshots demonstrate the successful implementation of the "Print Stock Need" feature:
✅ Button Integration: The print button is properly integrated into the Ultimate POS interface
✅ Data Display: All 6 columns are displaying correctly with proper formatting
✅ Professional Output: The print preview shows a clean, professional report layout
✅ Statistics Working: Summary statistics are calculating and displaying accurately
✅ PDF Export: Users can save the report as PDF or print directly
Key Achievements:
- Current Stock: 8.00 Pc(s) - displaying correctly
- Alert Quantity: 11.00 Pc(s) - showing the reorder threshold
- Stock Need: 3 Pc(s) - calculated correctly (11 - 8 = 3)
- Stock Coverage: 72.7% - calculated as (8/11 * 100 = 72.7%)
- Color Coding: Stock Need shows in red since it's > 0 (critical)
Overview
This guide will walk you through adding a "Print Stock Need" button to the home dashboard's stock alert section, allowing users to generate a printable report of products that need restocking.
The "Print Stock Need" feature allows users to generate a printable report showing:
- Product names
- Locations
- Stock needed quantities
- Formatted with professional styling
Prerequisites
- Access to Ultimate POS codebase
- Understanding of Laravel Blade templates
- Basic JavaScript knowledge
- Familiarity with Ultimate POS structure
Step-by-Step Implementation
Step 1: Locate the Target File
Navigate to the home index view file:
resources/views/home/index.blade.php
Step 2: Find the Stock Alert Section
Look for the @can('stock_report.view')
section that contains the product stock alert card. The table structure should have these columns:
<table class="table table-bordered table-striped" id="stock_alert_table" style="width: 100%;">
<thead>
<tr>
<th style="min-width: 300px">@lang('sale.product')</th>
<th>@lang('business.location')</th>
<th>@lang('report.current_stock')</th>
<th>@lang('product.alert_quantity')</th>
<th>@lang('lang_v1.expected_stock')</th>
<th>@lang('lang_v1.stock_need')</th>
</tr>
</thead>
</table>
Step 3: Add the Print Button
Inside the header section, locate the div with classes tw-flex tw-items-center tw-flex-1 tw-min-w-0 tw-gap-1
and add the button:
<div class="tw-w-full sm:tw-w-1/2 md:tw-w-1/2 tw-flex tw-items-center tw-gap-2">
@if (count($all_locations) > 1)
{!! Form::select('stock_alert_location', $all_locations, null, [
'class' => 'form-control select2 tw-flex-grow',
'placeholder' => __('lang_v1.select_location'),
'id' => 'stock_alert_location',
]) !!}
@endif
<button id="print_alerte_de_stock" class="btn btn-primary tw-flex-none">
<i class="fa fa-print mr-1"></i>
@lang('messages.print') @lang('lang_v1.stock_need')
</button>
</div>
Step 4: Add Enhanced JavaScript Implementation
Add this JavaScript code at the bottom of your blade file, just before the closing @endcan
:
<script>
$(document).ready(function(){
// Configuration object for easy customization
const printConfig = {
title: '{{ __("lang_v1.stock_need") }}',
columns: {
product: {
index: 0,
label: '{{ __("sale.product") }}',
enabled: true
},
location: {
index: 1,
label: '{{ __("business.location") }}',
enabled: true
},
currentStock: {
index: 2,
label: '{{ __("report.current_stock") }}',
enabled: true
},
alertQuantity: {
index: 3,
label: '{{ __("product.alert_quantity") }}',
enabled: true
},
expectedStock: {
index: 4,
label: '{{ __("lang_v1.expected_stock") }}',
enabled: true
},
stockNeed: {
index: 5,
label: '{{ __("lang_v1.stock_need") }}',
enabled: true
}
},
styles: {
pageMargin: '10mm',
fontSize: '12px',
headerFontSize: '16px',
tableFontSize: '11px'
}
};
$('#print_alerte_de_stock').click(function(){
// Validate table data
if (!validateTableData()) {
return;
}
// Show loading state
const originalText = $(this).html();
$(this).html('<i class="fa fa-spinner fa-spin mr-1"></i> {{ __("lang_v1.processing") }}...');
$(this).prop('disabled', true);
// Generate print window
setTimeout(() => {
generatePrintWindow();
// Reset button state
$(this).html(originalText);
$(this).prop('disabled', false);
}, 500);
});
function validateTableData() {
const tableBody = $('#stock_alert_table tbody');
if (tableBody.length === 0) {
alert('{{ __("lang_v1.table_not_found") }}');
return false;
}
const rows = tableBody.find('tr');
if (rows.length === 0 || rows.find('td:contains("{{ __("lang_v1.no_data_available") }}")').length > 0) {
alert('{{ __("lang_v1.no_data_available_print") }}');
return false;
}
return true;
}
function generatePrintWindow() {
const printWindow = window.open('', '_blank', 'width=800,height=600');
if (!printWindow) {
alert('{{ __("lang_v1.popup_blocked") }}');
return;
}
const printContent = generatePrintContent();
printWindow.document.write(printContent);
printWindow.document.close();
printWindow.onload = function() {
printWindow.focus();
printWindow.print();
};
}
function generatePrintContent() {
const currentDate = moment().format('LLLL');
const businessName = '{{ session("business.name") }}';
const selectedLocation = getSelectedLocation();
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${printConfig.title}</title>
${generatePrintStyles()}
</head>
<body>
<div class="print-container">
${generatePrintHeader(businessName, selectedLocation)}
${generatePrintTable()}
${generatePrintFooter(currentDate)}
</div>
</body>
</html>
`;
}
function generatePrintStyles() {
return `
<style>
@media print {
@page {
margin: ${printConfig.styles.pageMargin};
size: A4;
}
body {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
}
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: ${printConfig.styles.fontSize};
line-height: 1.4;
color: #333;
}
.print-container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
}
.print-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #2563eb;
padding-bottom: 15px;
}
.business-name {
font-size: 20px;
font-weight: bold;
color: #1e40af;
margin-bottom: 5px;
}
.report-title {
font-size: ${printConfig.styles.headerFontSize};
font-weight: bold;
color: #374151;
margin: 10px 0 5px 0;
}
.report-subtitle {
font-size: 12px;
color: #6b7280;
margin-bottom: 15px;
}
.report-filters {
font-size: 11px;
color: #4b5563;
margin-top: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
font-weight: 600;
padding: 12px 8px;
text-align: left;
font-size: ${printConfig.styles.tableFontSize};
border: 1px solid #1e40af;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 10px 8px;
border: 1px solid #e5e7eb;
font-size: ${printConfig.styles.tableFontSize};
vertical-align: top;
}
tbody tr:nth-child(even) {
background-color: #f8fafc;
}
tbody tr:hover {
background-color: #e0f2fe;
}
.stock-need-highlight {
font-weight: bold;
color: #dc2626;
}
.print-footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #e5e7eb;
text-align: center;
font-size: 10px;
color: #6b7280;
}
.summary-stats {
margin: 20px 0;
padding: 15px;
background: #f1f5f9;
border-radius: 8px;
border: 1px solid #cbd5e1;
}
.stat-item {
display: inline-block;
margin-right: 30px;
font-size: 11px;
}
.stat-label {
font-weight: bold;
color: #475569;
}
.stat-value {
color: #1e40af;
font-weight: bold;
}
</style>
`;
}
function generatePrintHeader(businessName, selectedLocation) {
const currentDate = moment().format('MMMM DD, YYYY');
const currentTime = moment().format('h:mm A');
// Company Logo Code - Add this section
const logoUrl = '{{ asset("uploads/business_logos/" . session("business.logo")) }}';
const logoHtml = logoUrl && '{{ session("business.logo") }}' ?
`<img src="${logoUrl}" alt="Company Logo" style="max-height: 60px; margin-bottom: 10px;">` : '';
return `
<div class="print-header">
${logoHtml}
<div class="business-name">${businessName}</div>
<div class="report-title">${printConfig.title}</div>
<div class="report-subtitle">{{ __('lang_v1.generated_on') }}: ${currentDate} {{ __('lang_v1.at') }} ${currentTime}</div>
${selectedLocation ? `<div class="report-filters">{{ __('business.location') }}: ${selectedLocation}</div>` : ''}
</div>
`;
}
function generatePrintTable() {
let tableHtml = '<table><thead><tr>';
// Generate table headers for enabled columns
Object.keys(printConfig.columns).forEach(key => {
const column = printConfig.columns[key];
if (column.enabled) {
tableHtml += `<th>${column.label}</th>`;
}
});
tableHtml += '</tr></thead><tbody>';
// Generate table rows
let totalProducts = 0;
let totalStockNeed = 0;
let totalCurrentStock = 0;
let totalAlertQuantity = 0;
let criticalItems = 0;
$('#stock_alert_table tbody tr').each(function() {
const row = $(this);
if (row.find('td').length > 0) {
tableHtml += '<tr>';
Object.keys(printConfig.columns).forEach(key => {
const column = printConfig.columns[key];
if (column.enabled) {
let cellContent = row.find('td').eq(column.index).html() || '';
// Extract numeric values for calculations
let numericValue = 0;
if (cellContent.includes('<span')) {
const match = cellContent.match(/>([\d.,-]+)</);
if (match) {
numericValue = parseFloat(match[1].replace(/[^\d.-]/g, '')) || 0;
}
} else {
numericValue = parseFloat(cellContent.replace(/[^\d.-]/g, '')) || 0;
}
// Special formatting and calculations based on column
if (key === 'stockNeed') {
totalStockNeed += numericValue;
if (numericValue > 0) criticalItems++;
cellContent = `<span class="stock-need-highlight">${cellContent}</span>`;
} else if (key === 'currentStock') {
totalCurrentStock += numericValue;
} else if (key === 'alertQuantity') {
totalAlertQuantity += numericValue;
}
tableHtml += `<td>${cellContent}</td>`;
}
});
tableHtml += '</tr>';
totalProducts++;
}
});
tableHtml += '</tbody></table>';
// Add enhanced summary statistics
const summaryHtml = `
<div class="summary-stats">
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_products') }}:</span>
<span class="stat-value">${totalProducts}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.critical_items') }}:</span>
<span class="stat-value">${criticalItems}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_current_stock') }}:</span>
<span class="stat-value">${totalCurrentStock.toFixed(2)}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_stock_needed') }}:</span>
<span class="stat-value">${totalStockNeed.toFixed(2)}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.coverage_ratio') }}:</span>
<span class="stat-value">${totalAlertQuantity > 0 ? ((totalCurrentStock / totalAlertQuantity) * 100).toFixed(1) + '%' : 'N/A'}</span>
</div>
</div>
`;
return summaryHtml + tableHtml;
}
function generatePrintFooter(currentDate) {
return `
<div class="print-footer">
<p>{{ __('lang_v1.printed_on') }}: ${currentDate}</p>
<p>{{ __('lang_v1.generated_by') }}: {{ auth()->user()->first_name }} {{ auth()->user()->last_name }}</p>
<p>{{ __('lang_v1.system_generated_report') }}</p>
</div>
`;
}
function getSelectedLocation() {
const locationSelect = $('#stock_alert_location');
if (locationSelect.length > 0 && locationSelect.val()) {
return locationSelect.find('option:selected').text();
}
return null;
}
});
</script>
Step 5: Update Controller Method
Update your getProductStockAlert()
method in app/Http/Controllers/HomeController.php
:
public function getProductStockAlert()
{
if (request()->ajax()) {
$business_id = request()->session()->get('user.business_id');
$permitted_locations = auth()->user()->permitted_locations();
$products = $this->productUtil->getProductAlert($business_id, $permitted_locations);
return Datatables::of($products)
->editColumn('product', function ($row) {
if ($row->type == 'single') {
return $row->product.' ('.$row->sku.')';
} else {
return $row->product.' - '.$row->product_variation.' - '.$row->variation.' ('.$row->sub_sku.')';
}
})
->editColumn('location', function ($row) {
return $row->location ?? __('lang_v1.all_locations');
})
->editColumn('stock', function ($row) {
$stock = $row->stock ? $row->stock : 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $stock.'</span> '.$row->unit;
})
->editColumn('alert_quantity', function ($row) {
$alert_qty = $row->alert_quantity ?? 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $alert_qty.'</span> '.$row->unit;
})
->addColumn('expected_stock', function ($row) {
$current_stock = $row->stock ? $row->stock : 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $current_stock.'</span> '.$row->unit;
})
->addColumn('stock_need', function ($row) {
$current_stock = $row->stock ? $row->stock : 0;
$alert_quantity = $row->alert_quantity ?? 0;
$stock_need = max(0, $alert_quantity - $current_stock);
$class = $stock_need > 0 ? 'text-danger font-weight-bold' : 'text-success';
return '<span class="'.$class.'" data-is_quantity="true" data-currency_symbol=false>'.(float) $stock_need.'</span> '.$row->unit;
})
->removeColumn('product_id')
->removeColumn('type')
->removeColumn('sku')
->removeColumn('product_variation')
->removeColumn('variation')
->removeColumn('sub_sku')
->removeColumn('unit')
->rawColumns([2, 3, 4, 5])
->make(false);
}
}
Step 6: Update ProductUtil Method
Update your getProductAlert()
method in app/Utils/ProductUtil.php
:
public function getProductAlert($business_id, $permitted_locations = null)
{
$query = VariationLocationDetails::join(
'product_variations as pv',
'variation_location_details.product_variation_id',
'=',
'pv.id'
)
->join(
'variations as v',
'variation_location_details.variation_id',
'=',
'v.id'
)
->join(
'products as p',
'variation_location_details.product_id',
'=',
'p.id'
)
->leftjoin(
'business_locations as l',
'variation_location_details.location_id',
'=',
'l.id'
)
->leftjoin('units as u', 'p.unit_id', '=', 'u.id')
->where('p.business_id', $business_id)
->where('p.enable_stock', 1)
->where('p.is_inactive', 0)
->whereNull('v.deleted_at')
->whereNotNull('p.alert_quantity')
->whereRaw('variation_location_details.qty_available <= p.alert_quantity');
//Check for permitted locations of a user
if (!empty($permitted_locations)) {
if ($permitted_locations != 'all') {
$query->whereIn('variation_location_details.location_id', $permitted_locations);
}
}
if (! empty(request()->input('location_id'))) {
$query->where('variation_location_details.location_id', request()->input('location_id'));
}
$products = $query->select(
'p.name as product',
'l.name as location',
'variation_location_details.qty_available as stock',
'p.alert_quantity',
'p.type',
'p.sku',
'pv.name as product_variation',
'v.name as variation',
'v.sub_sku',
'u.short_name as unit'
)
->groupBy('variation_location_details.id')
->orderBy('stock', 'asc');
return $products;
}
Column Details
The stock alert table displays the following information:
Column | Description | Purpose |
---|---|---|
Product | Product name with variations | Identify which items need restocking |
Location | Business location/warehouse | Track stock needs by location |
Current Stock | Available quantity on hand | Current inventory level |
Alert Quantity | Minimum stock threshold | When to reorder (warning level) |
Expected Stock | Projected stock after pending orders | Future inventory prediction |
Stock Need | Required quantity to reach optimal level | How much to order |
Stock Calculation Logic
The system calculates stock needs based on:
- Current Stock: Actual available inventory
- Alert Quantity: Minimum threshold before reordering
- Expected Stock: Current + incoming orders - pending sales
- Stock Need: (Alert Quantity - Expected Stock) when negative indicates shortage
Advanced Customization Options
1. Customizing Columns
To modify which columns appear in the print report, edit the printConfig.columns
object:
columns: {
product: { enabled: true },
location: { enabled: true },
currentStock: { enabled: true },
alertQuantity: { enabled: true },
expectedStock: { enabled: false }, // Hide this column
stockNeed: { enabled: true }
}
2. Styling Customization
Modify the printConfig.styles
object for different styling:
styles: {
pageMargin: '15mm',
fontSize: '14px',
headerFontSize: '18px',
tableFontSize: '12px'
}
3. Adding Company Logo
The logo code is already included in the generatePrintHeader()
function. It uses Ultimate POS's standard logo storage location at public/uploads/business_logos/
.
Language Translations
Add these translations to lang/en/lang_v1.php
:
'stock_need' => 'Stock Need',
'print_selected_columns' => 'Print Selected Columns',
'printed_on' => 'Printed on',
'processing' => 'Processing',
'table_not_found' => 'Table not found',
'no_data_available_print' => 'No data available to print',
'popup_blocked' => 'Popup blocked. Please allow popups for this site.',
'generated_on' => 'Generated on',
'at' => 'at',
'total_products' => 'Total Products',
'total_stock_needed' => 'Total Stock Needed',
'critical_items' => 'Critical Items',
'total_current_stock' => 'Total Current Stock',
'coverage_ratio' => 'Stock Coverage',
'generated_by' => 'Generated by',
'system_generated_report' => 'This is a system generated report',
'export' => 'Export',
'export_pdf' => 'Export as PDF',
'export_excel' => 'Export as Excel',
'export_csv' => 'Export as CSV',
'all_categories' => 'All Categories',
'all_levels' => 'All Stock Levels',
'critical' => 'Critical',
'low' => 'Low',
'needed' => 'Needed',
'expected_stock' => 'Expected Stock',
Testing Checklist
- Button appears for users with correct permissions
- Print functionality works in all supported browsers
- Styling renders correctly in print preview
- All translations are working
- All 6 columns display correctly
- Stock Need calculations are accurate
- Currency formatting works properly
This implementation provides a robust, customizable print solution that integrates seamlessly with Ultimate POS's existing architecture and styling conventions.
💛 Support this project
Binance ID:
478036326