Dashboard update
This commit is contained in:
parent
3052d3e254
commit
13fa5d8eb6
|
|
@ -141,9 +141,18 @@ class SamashtiDashboard(models.AbstractModel):
|
|||
return []
|
||||
|
||||
@api.model
|
||||
def get_products_data(self):
|
||||
def get_dashboard_cards_data(self):
|
||||
all_prod = self.env['product.product'].search([('type', '=', 'consu')])
|
||||
all_category = all_prod.category_id
|
||||
all_category = all_prod.categ_id
|
||||
out_stock_prods = all_prod.filtered(lambda x:x.qty_available <= 0)
|
||||
low_stock_prods = self.env['stock.warehouse.orderpoint'].search([('qty_to_order', '>', 0)]).product_id
|
||||
return {
|
||||
'products_count': [len(all_prod),all_prod.ids],
|
||||
'category_count':len(all_category),
|
||||
'out_stock_prods': [len(out_stock_prods),out_stock_prods.ids],
|
||||
'low_stock_prods': [len(low_stock_prods),low_stock_prods.ids],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@api.model
|
||||
|
|
|
|||
|
|
@ -1,61 +1,157 @@
|
|||
/** @odoo-module **/
|
||||
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
|
||||
import { Component, onMounted, useState, onWillStart, useRef } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
|
||||
function formatFloat(value) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return "0.00";
|
||||
}
|
||||
return parseFloat(value).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export class StockDashboard extends Component {
|
||||
static props = {
|
||||
static props = {
|
||||
...standardActionServiceProps,
|
||||
};
|
||||
static template = "StockDashboard";
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.gridRef = useRef("gridContainer");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
|
||||
// Refs for debouncing
|
||||
this.filterTimeout = null;
|
||||
|
||||
this.state = useState({
|
||||
rows: [],
|
||||
category:[],
|
||||
cards_data:[],
|
||||
filteredRows: [],
|
||||
fromDate: "",
|
||||
toDate: ""
|
||||
toDate: "",
|
||||
filters: {
|
||||
productCode: '',
|
||||
productName: '',
|
||||
minStock: '',
|
||||
maxStock: ''
|
||||
},
|
||||
sortField: '',
|
||||
sortDirection: 'asc',
|
||||
activeCategory: 'Finished Products'
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.loadDependencies();
|
||||
this.initializeDates();
|
||||
await this.loadGridData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
this.initDatePickers();
|
||||
if (this.gridRef.el) {
|
||||
this.renderGrid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadDependencies() {
|
||||
try {
|
||||
window.$ = window.jQuery = window.$ || window.jQuery;
|
||||
if (!window.pq) throw new Error("pqGrid failed to load");
|
||||
} catch (error) {
|
||||
console.error("Failed to load dependencies:", error);
|
||||
this.notification.add("Failed to load required components", { type: "danger" });
|
||||
throw error;
|
||||
// Get category groups (Finished Products vs Raw Materials & Packaging)
|
||||
getCategoryGroups() {
|
||||
const finishedProducts = this.getFilteredFinishedProducts();
|
||||
const rawMaterials = this.getFilteredRawMaterials();
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Finished Products',
|
||||
count: finishedProducts.length,
|
||||
icon: 'fas fa-box',
|
||||
color: 'primary',
|
||||
badgeClass: 'bg-primary bg-opacity-10 text-primary',
|
||||
slug: 'finished-products'
|
||||
},
|
||||
{
|
||||
name: 'Raw Materials & Packaging',
|
||||
count: rawMaterials.length,
|
||||
icon: 'fas fa-industry',
|
||||
color: 'success',
|
||||
badgeClass: 'bg-success bg-opacity-10 text-success',
|
||||
slug: 'raw-materials-packaging'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get ALL finished products (unfiltered)
|
||||
getFinishedProducts() {
|
||||
return this.state.rows.filter(row =>
|
||||
(row.category || '').toLowerCase() === 'finished products'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
OpenAllProducts() {
|
||||
let ids = this.state.cards_data['products_count'][1] || []
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: ("Products"),
|
||||
res_model: "product.product",
|
||||
views: [[false, "list"]],
|
||||
domain: [["id", "in", ids]],
|
||||
target: 'current'
|
||||
});
|
||||
}
|
||||
OpenOutofProducts() {
|
||||
let ids = this.state.cards_data['out_stock_prods'][1] || []
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: ("Products"),
|
||||
res_model: "product.product",
|
||||
views: [[false, "list"]],
|
||||
domain: [["id", "in", ids]],
|
||||
target: 'current'
|
||||
});
|
||||
}
|
||||
|
||||
OpenLowProducts() {
|
||||
let ids = this.state.cards_data['low_stock_prods'][1] || []
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: ("Products"),
|
||||
res_model: "stock.warehouse.orderpoint",
|
||||
views: [[false, "list"]],
|
||||
domain: [["product_id", "in", ids]],
|
||||
target: 'current'
|
||||
});
|
||||
}
|
||||
|
||||
// Get FILTERED finished products
|
||||
getFilteredFinishedProducts() {
|
||||
let filtered = this.getFinishedProducts();
|
||||
filtered = this.applyCustomFilters(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Get ALL raw materials & packaging (unfiltered)
|
||||
getRawMaterials() {
|
||||
return this.state.rows.filter(row => {
|
||||
const category = (row.category || '').toLowerCase();
|
||||
return category !== 'finished products' && category !== '';
|
||||
});
|
||||
}
|
||||
|
||||
// Get FILTERED raw materials
|
||||
getFilteredRawMaterials() {
|
||||
let filtered = this.getRawMaterials();
|
||||
filtered = this.applyCustomFilters(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Switch active category
|
||||
switchCategory(categoryName) {
|
||||
this.state.activeCategory = categoryName;
|
||||
// Trigger filter update to show correct filtered data
|
||||
this.updateFilter();
|
||||
}
|
||||
|
||||
// Format float values for display
|
||||
formatFloat(value) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return "0.00";
|
||||
}
|
||||
return parseFloat(value).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
initializeDates() {
|
||||
|
|
@ -99,230 +195,507 @@ export class StockDashboard extends Component {
|
|||
this.state.toDate,
|
||||
]);
|
||||
this.state.rows = records || [];
|
||||
this.renderGrid();
|
||||
// Initialize filteredRows with filtered data
|
||||
this.updateFilter();
|
||||
this.state.cards_data = await this.orm.call("samashti.board", "get_dashboard_cards_data",[])
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
this.notification.add("Error loading stock data", { type: "danger" });
|
||||
}
|
||||
}
|
||||
getTotalStockValue() {
|
||||
return formatFloat(this.state.rows.reduce((sum, row) => sum + (row.value || 0), 0).toFixed(2)) + " ₹";
|
||||
|
||||
getTotalStockValue() {
|
||||
return this.formatFloat(this.state.rows.reduce((sum, row) => sum + (row.value || 0), 0).toFixed(2)) + " ₹";
|
||||
}
|
||||
parseFloatValue(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
|
||||
parseFloatValue(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
let cleaned = value.replace(/[^\d.-]/g, '');
|
||||
let result = parseFloat(cleaned);
|
||||
return isNaN(result) ? 0 : result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
let cleaned = value.replace(/[^\d.-]/g, '');
|
||||
let result = parseFloat(cleaned);
|
||||
return isNaN(result) ? 0 : result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Stock dashboard calculation methods
|
||||
getTotalProductsCount() {
|
||||
// Count unique products based on product_code
|
||||
const uniqueProducts = new Set(
|
||||
this.state.rows
|
||||
.filter(row => row.product_code)
|
||||
.map(row => row.product_code)
|
||||
);
|
||||
return uniqueProducts.size;
|
||||
}
|
||||
|
||||
getCategoryCount() {
|
||||
// Count unique categories
|
||||
const uniqueCategories = new Set(
|
||||
this.state.rows
|
||||
.filter(row => row.category)
|
||||
.map(row => row.category)
|
||||
);
|
||||
return uniqueCategories.size;
|
||||
}
|
||||
|
||||
|
||||
|
||||
getOutOfStockCount() {
|
||||
return this.state.rows.filter(row => {
|
||||
const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
return currentStock <= 0;
|
||||
}).length;
|
||||
}
|
||||
|
||||
getLowStockCount() {
|
||||
return this.state.rows.filter(row => {
|
||||
const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
const minStock = this.parseFloatValue(row.min_stock) || 50; // Default minimum stock
|
||||
return currentStock > 0 && currentStock < minStock;
|
||||
}).length;
|
||||
}
|
||||
|
||||
getLowStockPercentage() {
|
||||
const totalProducts = this.getTotalProductsCount();
|
||||
const lowStockCount = this.getLowStockCount();
|
||||
|
||||
if (totalProducts === 0) return "0.00";
|
||||
|
||||
const percentage = (lowStockCount / totalProducts) * 100;
|
||||
return formatFloat(percentage);
|
||||
}
|
||||
async getColumns() {
|
||||
return [
|
||||
{ title: "Product Code", dataIndx: "product_code", width: 100, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Product Name", dataIndx: "product_name", width: 280, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Category", dataIndx: "category", width: 150 },
|
||||
{ title: "Opening Stock", dataIndx: "opening_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Receipts", dataIndx: "receipts", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Production", dataIndx: "production", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Consumption", dataIndx: "consumption", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Dispatch", dataIndx: "dispatch", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Closing Stock", dataIndx: "closing_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Uom", dataIndx: "uom", width: 90, dataType: "text" },
|
||||
{ title: "Value", dataIndx: "value", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
];
|
||||
getTotalProductsCount() {
|
||||
const uniqueProducts = new Set(
|
||||
this.state.rows
|
||||
.filter(row => row.product_code)
|
||||
.map(row => row.product_code)
|
||||
);
|
||||
return uniqueProducts.size;
|
||||
}
|
||||
|
||||
async renderGrid() {
|
||||
const columns = await this.getColumns();
|
||||
const agg = pq.aggregate;
|
||||
|
||||
const gridOptions = {
|
||||
selectionModel: { type: "row" },
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
editable: false,
|
||||
freezeCols: 2,
|
||||
stripeRows: true,
|
||||
menuIcon: true, //show header menu icon initially.
|
||||
menuUI: {
|
||||
tabs: ['filter'] //display only filter tab.
|
||||
},
|
||||
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1,menuIcon: true },
|
||||
dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" },
|
||||
colModel: columns,
|
||||
toolbar: {
|
||||
items: [
|
||||
{
|
||||
type: 'select',
|
||||
label: '<span style="color: #555; font-size: 13px;">Format:</span>',
|
||||
attr: 'id="export_format" style="margin-left: 10px; margin-right: 20px; padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px; background: #fafafa; color: #333; font-size: 13px; min-width: 110px;"',
|
||||
options: [{ xlsx: '📊 Excel', csv: '📝 CSV', htm: '🌐 HTML', json: '🔤 JSON' }]
|
||||
},
|
||||
{
|
||||
type: "button", label: "Export", icon: "ui-icon-arrowthickstop-1-s",
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: () => this.exportGrid()
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ui-icon-print',
|
||||
label: 'Print',
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: function () {
|
||||
var exportHtml = this.exportData({
|
||||
title: 'Stock Report',
|
||||
format: 'htm',
|
||||
render: true
|
||||
}),
|
||||
newWin = window.open('', '', 'width=1200, height=700'),
|
||||
doc = newWin.document.open();
|
||||
doc.write(exportHtml);
|
||||
doc.close();
|
||||
newWin.print();
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
detailModel: {
|
||||
cache: false,
|
||||
getCategoryCount() {
|
||||
const categories = new Set();
|
||||
this.state.rows.forEach(row => {
|
||||
const category = (row.category || '').toLowerCase();
|
||||
if (category === 'finished products') {
|
||||
categories.add('Finished Products');
|
||||
} else if (category) {
|
||||
categories.add('Raw Materials & Packaging');
|
||||
}
|
||||
};
|
||||
|
||||
var grid = $(this.gridRef.el).pqGrid()
|
||||
if (grid.length) {
|
||||
grid.pqGrid("destroy");
|
||||
}
|
||||
|
||||
$(this.gridRef.el)
|
||||
.css({ height: '600px', width: '100%' })
|
||||
.pqGrid(gridOptions);
|
||||
|
||||
const groupModel = {
|
||||
on: true,
|
||||
checkbox: true,
|
||||
checkboxHead: true,
|
||||
header: true,
|
||||
cascade: true,
|
||||
titleInFirstCol: true,
|
||||
fixCols: true,
|
||||
showSummary: [true, true],
|
||||
grandSummary: true,
|
||||
title: [
|
||||
"{0} ({1})",
|
||||
"{0} - {1}"
|
||||
]
|
||||
};
|
||||
$(this.gridRef.el).pqGrid("groupOption", groupModel);
|
||||
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||
});
|
||||
return categories.size;
|
||||
}
|
||||
|
||||
exportGrid() {
|
||||
const format_ex = $("#export_format").val();
|
||||
const grid = $(this.gridRef.el);
|
||||
getOutOfStockCount() {
|
||||
return this.state.rows.filter(row => {
|
||||
const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
return currentStock <= 0;
|
||||
}).length;
|
||||
}
|
||||
|
||||
switch (format_ex) {
|
||||
case 'xlsx':
|
||||
const blobXlsx = grid.pqGrid("exportData", {
|
||||
format: 'xlsx',
|
||||
render: true
|
||||
});
|
||||
saveAs(blobXlsx, "StockData.xlsx");
|
||||
break;
|
||||
getLowStockCount() {
|
||||
return this.state.rows.filter(row => {
|
||||
const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
const minStock = this.parseFloatValue(row.min_stock) || 50;
|
||||
return currentStock > 0 && currentStock < minStock;
|
||||
}).length;
|
||||
}
|
||||
|
||||
case 'csv':
|
||||
const blobCsv = grid.pqGrid("exportData", {
|
||||
format: 'csv',
|
||||
render: true
|
||||
});
|
||||
if (typeof blobCsv === 'string') {
|
||||
saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv");
|
||||
} else {
|
||||
saveAs(blobCsv, "StockData.csv");
|
||||
}
|
||||
break;
|
||||
// Apply custom filters to a dataset
|
||||
applyCustomFilters(data) {
|
||||
let filtered = [...data];
|
||||
|
||||
case 'htm':
|
||||
case 'html':
|
||||
const blobHtml = grid.pqGrid("exportData", {
|
||||
format: 'htm',
|
||||
render: true
|
||||
});
|
||||
if (typeof blobHtml === 'string') {
|
||||
saveAs(new Blob([blobHtml], { type: 'text/html' }), "StockData.html");
|
||||
} else {
|
||||
saveAs(blobHtml, "StockData.html");
|
||||
}
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
const blobJson = grid.pqGrid("exportData", {
|
||||
format: 'json',
|
||||
render: true
|
||||
});
|
||||
if (typeof blobJson === 'string') {
|
||||
saveAs(new Blob([blobJson], { type: 'application/json' }), "StockData.json");
|
||||
} else {
|
||||
saveAs(blobJson, "StockData.json");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
alert('Unsupported format: ' + format_ex);
|
||||
// Apply product code filter
|
||||
if (this.state.filters.productCode.trim() !== '') {
|
||||
filtered = filtered.filter(row => {
|
||||
const productCode = row.product_code || '';
|
||||
return productCode.toLowerCase().includes(this.state.filters.productCode.toLowerCase().trim());
|
||||
});
|
||||
}
|
||||
|
||||
// Apply product name filter
|
||||
if (this.state.filters.productName.trim() !== '') {
|
||||
filtered = filtered.filter(row => {
|
||||
const productName = row.product_name || '';
|
||||
return productName.toLowerCase().includes(this.state.filters.productName.toLowerCase().trim());
|
||||
});
|
||||
}
|
||||
|
||||
// Apply stock range filters
|
||||
if (this.state.filters.minStock !== '') {
|
||||
const min = this.parseFloatValue(this.state.filters.minStock);
|
||||
filtered = filtered.filter(row => {
|
||||
const stock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
return stock >= min;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.filters.maxStock !== '') {
|
||||
const max = this.parseFloatValue(this.state.filters.maxStock);
|
||||
filtered = filtered.filter(row => {
|
||||
const stock = this.parseFloatValue(row.closing_stock || row.quantity || 0);
|
||||
return stock <= max;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting if specified
|
||||
if (this.state.sortField) {
|
||||
filtered.sort((a, b) => {
|
||||
let valA = a[this.state.sortField];
|
||||
let valB = b[this.state.sortField];
|
||||
|
||||
// Handle undefined/null values
|
||||
if (valA === null || valA === undefined) valA = '';
|
||||
if (valB === null || valB === undefined) valB = '';
|
||||
|
||||
if (typeof valA === 'string') {
|
||||
valA = valA.toLowerCase();
|
||||
valB = valB.toLowerCase();
|
||||
}
|
||||
|
||||
if (valA < valB) return this.state.sortDirection === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return this.state.sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Keyup filter handler with debouncing
|
||||
onFilterKeyup(event) {
|
||||
const field = event.target.placeholder.toLowerCase().includes('code') ? 'productCode' :
|
||||
event.target.placeholder.toLowerCase().includes('name') ? 'productName' :
|
||||
event.target.placeholder.toLowerCase().includes('min') ? 'minStock' :
|
||||
event.target.placeholder.toLowerCase().includes('max') ? 'maxStock' : '';
|
||||
|
||||
if (field) {
|
||||
this.state.filters[field] = event.target.value;
|
||||
|
||||
// Clear existing timeout
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
|
||||
// Set new timeout for debouncing (300ms delay)
|
||||
this.filterTimeout = setTimeout(() => {
|
||||
this.updateFilter();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter handler - called after debounce or button click
|
||||
updateFilter() {
|
||||
// Clear any pending timeout
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
this.filterTimeout = null;
|
||||
}
|
||||
|
||||
// Update filteredRows based on active category
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
const filtered = this.getFilteredFinishedProducts();
|
||||
this.state.filteredRows = filtered;
|
||||
} else {
|
||||
const filtered = this.getFilteredRawMaterials();
|
||||
this.state.filteredRows = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
// Clear timeout if any
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
this.filterTimeout = null;
|
||||
}
|
||||
|
||||
this.state.filters = {
|
||||
productCode: '',
|
||||
productName: '',
|
||||
minStock: '',
|
||||
maxStock: ''
|
||||
};
|
||||
this.state.sortField = '';
|
||||
this.state.sortDirection = 'asc';
|
||||
|
||||
// Reset to show all data based on active category
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
this.state.filteredRows = this.getFinishedProducts();
|
||||
} else {
|
||||
this.state.filteredRows = this.getRawMaterials();
|
||||
}
|
||||
}
|
||||
|
||||
sortTable(field) {
|
||||
if (this.state.sortField === field) {
|
||||
this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.state.sortField = field;
|
||||
this.state.sortDirection = 'asc';
|
||||
}
|
||||
|
||||
// Re-apply filters with new sort
|
||||
this.updateFilter();
|
||||
}
|
||||
|
||||
// Export methods
|
||||
exportToCSV() {
|
||||
let headers, rows;
|
||||
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
const filteredData = this.getFilteredFinishedProducts();
|
||||
headers = ['Product Code', 'Product Name', 'Opening Stock', 'Production', 'Dispatch', 'Closing Stock', 'UOM', 'Value'];
|
||||
rows = filteredData.map(row => [
|
||||
`"${row.product_code || ''}"`,
|
||||
`"${row.product_name || ''}"`,
|
||||
row.opening_stock || 0,
|
||||
row.production || 0,
|
||||
row.dispatch || 0,
|
||||
row.closing_stock || 0,
|
||||
`"${row.uom || ''}"`,
|
||||
row.value || 0
|
||||
]);
|
||||
} else {
|
||||
const filteredData = this.getFilteredRawMaterials();
|
||||
headers = ['Product Code', 'Product Name', 'Opening Stock', 'Receipts', 'Consumption', 'Closing Stock', 'UOM', 'Value'];
|
||||
rows = filteredData.map(row => [
|
||||
`"${row.product_code || ''}"`,
|
||||
`"${row.product_name || ''}"`,
|
||||
row.opening_stock || 0,
|
||||
row.receipts || 0,
|
||||
row.consumption || 0,
|
||||
row.closing_stock || 0,
|
||||
`"${row.uom || ''}"`,
|
||||
row.value || 0
|
||||
]);
|
||||
}
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.join(','))
|
||||
].join('\n');
|
||||
|
||||
const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.csv`;
|
||||
this.downloadFile(csvContent, fileName, 'text/csv');
|
||||
}
|
||||
|
||||
exportToExcel() {
|
||||
let tableHeaders, rows;
|
||||
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
const filteredData = this.getFilteredFinishedProducts();
|
||||
tableHeaders = `
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<th>Product Name</th>
|
||||
<th>Opening Stock</th>
|
||||
<th>Production</th>
|
||||
<th>Dispatch</th>
|
||||
<th>Closing Stock</th>
|
||||
<th>UOM</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
rows = filteredData.map(row => `
|
||||
<tr>
|
||||
<td>${row.product_code || ''}</td>
|
||||
<td>${row.product_name || ''}</td>
|
||||
<td>${row.opening_stock || 0}</td>
|
||||
<td>${row.production || 0}</td>
|
||||
<td>${row.dispatch || 0}</td>
|
||||
<td>${row.closing_stock || 0}</td>
|
||||
<td>${row.uom || ''}</td>
|
||||
<td>${row.value || 0}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
const filteredData = this.getFilteredRawMaterials();
|
||||
tableHeaders = `
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<th>Product Name</th>
|
||||
<th>Opening Stock</th>
|
||||
<th>Receipts</th>
|
||||
<th>Consumption</th>
|
||||
<th>Closing Stock</th>
|
||||
<th>UOM</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
rows = filteredData.map(row => `
|
||||
<tr>
|
||||
<td>${row.product_code || ''}</td>
|
||||
<td>${row.product_name || ''}</td>
|
||||
<td>${row.opening_stock || 0}</td>
|
||||
<td>${row.receipts || 0}</td>
|
||||
<td>${row.consumption || 0}</td>
|
||||
<td>${row.closing_stock || 0}</td>
|
||||
<td>${row.uom || ''}</td>
|
||||
<td>${row.value || 0}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
const tableHtml = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid black; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<h2>${this.state.activeCategory} Report - ${this.state.fromDate} to ${this.state.toDate}</h2>
|
||||
<table>
|
||||
${tableHeaders}
|
||||
${rows}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.xls`;
|
||||
this.downloadFile(tableHtml, fileName, 'application/vnd.ms-excel');
|
||||
}
|
||||
|
||||
exportToJSON() {
|
||||
let data;
|
||||
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
data = this.getFilteredFinishedProducts();
|
||||
} else {
|
||||
data = this.getFilteredRawMaterials();
|
||||
}
|
||||
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.json`;
|
||||
this.downloadFile(jsonData, fileName, 'application/json');
|
||||
}
|
||||
|
||||
downloadFile(content, fileName, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
printTable() {
|
||||
const printWindow = window.open('', '_blank');
|
||||
|
||||
let tableHeaders, rows, totals;
|
||||
|
||||
if (this.state.activeCategory === 'Finished Products') {
|
||||
const finishedProducts = this.getFilteredFinishedProducts();
|
||||
tableHeaders = `
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<th>Product Name</th>
|
||||
<th>Opening Stock</th>
|
||||
<th>Production</th>
|
||||
<th>Dispatch</th>
|
||||
<th>Closing Stock</th>
|
||||
<th>UOM</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
rows = finishedProducts.map(row => {
|
||||
let rowClass = '';
|
||||
if (row.closing_stock <= 0) {
|
||||
rowClass = 'danger';
|
||||
} else if (row.closing_stock < 50) {
|
||||
rowClass = 'warning';
|
||||
}
|
||||
return `
|
||||
<tr class="${rowClass}">
|
||||
<td>${row.product_code || ''}</td>
|
||||
<td>${row.product_name || ''}</td>
|
||||
<td>${this.formatFloat(row.opening_stock)}</td>
|
||||
<td>${this.formatFloat(row.production)}</td>
|
||||
<td>${this.formatFloat(row.dispatch)}</td>
|
||||
<td>${this.formatFloat(row.closing_stock)}</td>
|
||||
<td>${row.uom || ''}</td>
|
||||
<td>${this.formatFloat(row.value)} ₹</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
totals = `
|
||||
<tr class="total-row">
|
||||
<td colspan="2"><strong>TOTALS</strong></td>
|
||||
<td><strong>${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.opening_stock || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.production || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.dispatch || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.closing_stock || 0), 0))}</strong></td>
|
||||
<td></td>
|
||||
<td><strong>${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.value || 0), 0))} ₹</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
} else {
|
||||
const rawMaterials = this.getFilteredRawMaterials();
|
||||
tableHeaders = `
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<th>Product Name</th>
|
||||
<th>Opening Stock</th>
|
||||
<th>Receipts</th>
|
||||
<th>Consumption</th>
|
||||
<th>Closing Stock</th>
|
||||
<th>UOM</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
rows = rawMaterials.map(row => {
|
||||
let rowClass = '';
|
||||
if (row.closing_stock <= 0) {
|
||||
rowClass = 'danger';
|
||||
} else if (row.closing_stock < 50) {
|
||||
rowClass = 'warning';
|
||||
}
|
||||
return `
|
||||
<tr class="${rowClass}">
|
||||
<td>${row.product_code || ''}</td>
|
||||
<td>${row.product_name || ''}</td>
|
||||
<td>${this.formatFloat(row.opening_stock)}</td>
|
||||
<td>${this.formatFloat(row.receipts)}</td>
|
||||
<td>${this.formatFloat(row.consumption)}</td>
|
||||
<td>${this.formatFloat(row.closing_stock)}</td>
|
||||
<td>${row.uom || ''}</td>
|
||||
<td>${this.formatFloat(row.value)} ₹</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
totals = `
|
||||
<tr class="total-row">
|
||||
<td colspan="2"><strong>TOTALS</strong></td>
|
||||
<td><strong>${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.opening_stock || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.receipts || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.consumption || 0), 0))}</strong></td>
|
||||
<td><strong>${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.closing_stock || 0), 0))}</strong></td>
|
||||
<td></td>
|
||||
<td><strong>${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.value || 0), 0))} ₹</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>${this.state.activeCategory} Report</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; border: 2px solid #333; }
|
||||
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
|
||||
th { background-color: #f2f2f2; font-weight: bold; }
|
||||
.header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #333; padding-bottom: 20px; }
|
||||
.footer { margin-top: 30px; font-size: 12px; color: #666; border-top: 1px solid #ddd; padding-top: 20px; }
|
||||
.total-row { background-color: #f8f9fa; font-weight: bold; }
|
||||
.warning { background-color: #fff3cd; }
|
||||
.danger { background-color: #f8d7da; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>${this.state.activeCategory} Report</h1>
|
||||
<h3>Date Range: ${this.state.fromDate} to ${this.state.toDate}</h3>
|
||||
<p>Generated on: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
${tableHeaders}
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
${totals}
|
||||
</tfoot>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Helper method to get sort icon
|
||||
getSortIcon(field) {
|
||||
if (this.state.sortField !== field) {
|
||||
return 'fas fa-sort';
|
||||
}
|
||||
return this.state.sortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("StockDashboard", StockDashboard);
|
||||
registry.category("actions").add("StockDashboard", StockDashboard);
|
||||
|
|
@ -1,135 +1,848 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="StockDashboard" owl="1">
|
||||
<div class="p-4" style="height: 100%; overflow-y: auto;">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<!-- Left Side: Title with Icon -->
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary rounded-circle p-2 me-3">
|
||||
<i class="fa fa-cubes text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-primary fw-bold mb-1">Stock Dashboard</h4>
|
||||
<p class="text-muted mb-0 small">
|
||||
<i class="fa fa-calendar me-1"></i>
|
||||
<span t-esc="state.fromDate || 'Start Date'"/>
|
||||
<i class="fa fa-arrow-right mx-2"></i>
|
||||
<span t-esc="state.toDate || 'End Date'"/>
|
||||
</p>
|
||||
<div class="bg-gradient-light" style="height: 750px; overflow-y: auto;">
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<!-- Enhanced Header -->
|
||||
<div class="glass-card rounded-4 mb-4">
|
||||
<div class="p-4">
|
||||
<div class="row align-items-center">
|
||||
<!-- Left Side: Brand Header -->
|
||||
<div class="col-lg-7 mb-3 mb-lg-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-gradient-primary rounded-3 p-3 me-3 shadow-sm">
|
||||
<i class="fas fa-chart-line text-white fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-dark fw-bold mb-1">Stock Analytics Dashboard</h3>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<span class="badge bg-light text-dark border border-secondary-subtle px-3 py-2 rounded-pill">
|
||||
<i class="fas fa-calendar-alt me-2"></i>
|
||||
<span t-esc="state.fromDate || 'Start Date'"/>
|
||||
<i class="fas fa-arrow-right mx-2"></i>
|
||||
<span t-esc="state.toDate || 'End Date'"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Modern Date Controls -->
|
||||
<div class="col-lg-5">
|
||||
<div class="row g-2 align-items-center justify-content-end">
|
||||
<div class="col-12 col-sm-6">
|
||||
<div class="input-group input-group-sm shadow-sm">
|
||||
<!-- <span class="input-group-text bg-white border-end-0">-->
|
||||
<!-- <i class="fas fa-calendar-day text-primary"></i>-->
|
||||
<!-- </span>-->
|
||||
<input type="date" id="fromDate" t-model="state.fromDate"
|
||||
class="form-control form-control-sm border-start-0 py-2"
|
||||
style="min-width: 100px;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6">
|
||||
<div class="input-group input-group-sm shadow-sm">
|
||||
<!-- <span class="input-group-text bg-white border-end-0">-->
|
||||
<!-- <i class="fas fa-calendar-day text-primary"></i>-->
|
||||
<!-- </span>-->
|
||||
<input type="date" id="toDate" t-model="state.toDate"
|
||||
class="form-control form-control-sm border-start-0 py-2"
|
||||
style="min-width: 100px;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-primary btn-sm px-4 py-2 rounded-3 shadow-sm flex-grow-1 d-flex align-items-center justify-content-center"
|
||||
t-on-click="loadGridData">
|
||||
<i class="fas fa-sync-alt me-2"></i>
|
||||
<span class="fw-medium">Update Dashboard</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Date Controls -->
|
||||
<div class="col-md-6">
|
||||
<div class="row g-2 align-items-center justify-content-end">
|
||||
<div class="col-auto">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</span>
|
||||
<input type="text" id="fromDate" t-model="state.fromDate"
|
||||
class="form-control form-control-sm" placeholder="From Date"/>
|
||||
<!-- Enhanced Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Total Products Card -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100 hover-lift">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-start" t-on-click="() => this.OpenAllProducts()">
|
||||
<div class="bg-primary bg-opacity-10 rounded-3 p-3 me-3">
|
||||
<i class="fas fa-boxes text-white fs-2"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-muted small mb-1">TOTAL PRODUCTS</div>
|
||||
<h3 class="mb-2 fw-bold text-dark" t-esc="state.cards_data['products_count'][0]"/>
|
||||
<div class="text-muted small">
|
||||
<i class="fas fa-tags me-1"></i>
|
||||
<span t-esc="state.cards_data['category_count'] + ' categories'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</span>
|
||||
<input type="text" id="toDate" t-model="state.toDate"
|
||||
class="form-control form-control-sm" placeholder="To Date"/>
|
||||
<!-- Current Stock Value Card -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100 hover-lift">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="bg-success bg-opacity-10 rounded-3 p-3 me-3">
|
||||
<i class="fas fa-dollar-sign text-white fs-2"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-muted small mb-1">STOCK VALUE</div>
|
||||
<h3 class="mb-2 fw-bold text-success" t-esc="this.getTotalStockValue()"/>
|
||||
<div class="text-muted small">
|
||||
<i class="fas fa-warehouse me-1"></i>
|
||||
Total inventory value
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary btn-sm" t-on-click="loadGridData">
|
||||
<i class="fa fa-refresh me-1"></i> Update
|
||||
</button>
|
||||
<!-- Out of Stock Products Card -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100 hover-lift">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-start" t-on-click="() => this.OpenOutofProducts()">
|
||||
<div class="bg-danger bg-opacity-10 rounded-3 p-3 me-3">
|
||||
<i class="fas fa-ban text-white fs-2"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-muted small mb-1">OUT OF STOCK</div>
|
||||
<h3 class="mb-2 fw-bold text-danger" t-esc="state.cards_data['out_stock_prods'][0]"/>
|
||||
<div class="text-muted small">
|
||||
<i class="fas fa-exclamation-circle me-1"></i>
|
||||
Needs restocking
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Low Stock Alert Card -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100 hover-lift">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-start" t-on-click="() => this.OpenLowProducts()">
|
||||
<div class="bg-warning bg-opacity-10 rounded-3 p-3 me-3">
|
||||
<i class="fas fa-exclamation-triangle text-white fs-2"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-muted small mb-1">LOW STOCK</div>
|
||||
<h3 class="mb-2 fw-bold text-warning" t-esc="state.cards_data['low_stock_prods'][0]"/>
|
||||
<div class="text-muted small">
|
||||
<i class="fas fa-layer-group me-1"></i>
|
||||
Below minimum levels
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="card border-0 shadow-sm mb-4 rounded-4">
|
||||
<div class="card-header bg-white border-0 py-3 px-4">
|
||||
<h5 class="mb-0 fw-semibold text-dark">Stock by Category</h5>
|
||||
<small class="text-muted">Click on a category to view detailed inventory</small>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<!-- Category Tabs Navigation -->
|
||||
<ul class="nav nav-tabs nav-tabs-custom mb-4" role="tablist">
|
||||
<t t-foreach="this.getCategoryGroups()" t-as="category" t-key="category.name">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link"
|
||||
t-att-class="category.name === state.activeCategory ? 'active' : ''"
|
||||
data-bs-toggle="tab"
|
||||
t-att-data-bs-target="'#' + category.slug"
|
||||
type="button"
|
||||
role="tab"
|
||||
t-on-click="() => this.switchCategory(category.name)">
|
||||
<i t-att-class="category.icon + ' me-2'"></i>
|
||||
<t t-esc="category.name"/>
|
||||
<span t-att-class="'badge ' + category.badgeClass + ' ms-2'">
|
||||
<t t-esc="category.count" t-att-class="'text-white'"/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
<!-- Category Tabs Content -->
|
||||
<div class="tab-content">
|
||||
<!-- Finished Products Tab -->
|
||||
<div class="tab-pane fade"
|
||||
t-att-class="state.activeCategory === 'Finished Products' ? 'show active' : ''"
|
||||
id="finished-products"
|
||||
role="tabpanel">
|
||||
<!-- Finished Products Table -->
|
||||
<div class="card border-0 shadow-lg rounded-4 overflow-hidden" style="border: 2px solid #dee2e6;">
|
||||
<div class="card-header bg-gradient-primary border-0 py-3">
|
||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between">
|
||||
<div class="d-flex align-items-center mb-2 mb-md-0">
|
||||
<div class="bg-opacity-20 rounded-3 p-2 me-3">
|
||||
<i class="fas fa-box text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0 text-white fw-semibold">Finished Products</h5>
|
||||
<small class="text-white text-opacity-75">
|
||||
Ready for dispatch products
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-light btn-sm px-3 rounded-3 d-flex align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-download me-2"></i>Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToCSV">
|
||||
<i class="fas fa-file-csv me-2"></i>CSV
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToExcel">
|
||||
<i class="fas fa-file-excel me-2"></i>Excel
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToJSON">
|
||||
<i class="fas fa-file-code me-2"></i>JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm px-3 rounded-3 d-flex align-items-center"
|
||||
t-on-click="printTable">
|
||||
<i class="fas fa-print me-2"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Row -->
|
||||
<div class="table-filters p-3 border-bottom bg-light">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Product Code"
|
||||
t-model="state.filters.productCode"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Product Name"
|
||||
t-model="state.filters.productName"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Min Stock"
|
||||
t-model="state.filters.minStock"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Max Stock"
|
||||
t-model="state.filters.maxStock"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
t-on-click="clearFilters">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
Showing <span t-esc="this.getFilteredFinishedProducts().length"/> of <span t-esc="this.getFinishedProducts().length"/> records
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
Total Value: <span t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.value || 0), 0))"/> ₹
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Table Container -->
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead class="table-light sticky-top" style="top: 0; z-index: 1;">
|
||||
<tr>
|
||||
<th style="min-width: 60px; text-align:center;">S.No</th>
|
||||
<th t-on-click="() => this.sortTable('product_code')"
|
||||
style="cursor: pointer; min-width: 100px;">
|
||||
Product Code
|
||||
<i t-att-class="this.getSortIcon('product_code')" class="ms-1"></i>
|
||||
</th>
|
||||
<th t-on-click="() => this.sortTable('product_name')"
|
||||
style="cursor: pointer; min-width: 300px;">
|
||||
Product Name
|
||||
<i t-att-class="this.getSortIcon('product_name')" class="ms-1"></i>
|
||||
</th>
|
||||
<th style="min-width: 120px; text-align: right;">Opening Stock</th>
|
||||
<th style="min-width: 120px; text-align: right;">Production</th>
|
||||
<th style="min-width: 120px; text-align: right;">Dispatch</th>
|
||||
<th t-on-click="() => this.sortTable('closing_stock')"
|
||||
style="cursor: pointer; min-width: 120px; text-align: right;">
|
||||
Closing Stock
|
||||
<i t-att-class="this.getSortIcon('closing_stock')" class="ms-1"></i>
|
||||
</th>
|
||||
<th style="min-width: 80px;">UOM</th>
|
||||
<th style="min-width: 120px; text-align: right;">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="this.getFilteredFinishedProducts()" t-as="row" t-key="row_index">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<t t-esc="row_index + 1"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-esc="row.product_code || 'N/A'"/>
|
||||
</td>
|
||||
<td t-esc="row.product_name || 'N/A'"/>
|
||||
<td class="text-end" t-esc="this.formatFloat(row.opening_stock)"/>
|
||||
<td class="text-end">
|
||||
<span class="text-info" t-esc="this.formatFloat(row.production)"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="text-danger" t-esc="this.formatFloat(row.dispatch)"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-esc="this.formatFloat((row.opening_stock + row.production) - row.dispatch) > 0 ? this.formatFloat((row.opening_stock + row.production) - row.dispatch): 0"/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary px-2 py-1">
|
||||
<t t-esc="row.uom || 'N/A'"/>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<span class="text-success">
|
||||
<t t-esc="this.formatFloat(row.value)"/> ₹
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="this.getFilteredFinishedProducts().length === 0">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="fas fa-database fa-2x mb-3"></i>
|
||||
<p class="mb-0">No finished products available</p>
|
||||
<small>Try adjusting your filters or loading more data</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
<tfoot t-if="this.getFilteredFinishedProducts().length > 0">
|
||||
<tr class="table-light">
|
||||
<td colspan="3" class="fw-bold">Totals</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.opening_stock || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.production || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.dispatch || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.closing_stock || 0), 0))"/>
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="text-end fw-bold text-success">
|
||||
<t t-esc="this.formatFloat(this.getFilteredFinishedProducts().reduce((sum, row) => sum + (row.value || 0), 0))"/> ₹
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Padding -->
|
||||
<div class="card-footer bg-transparent border-0 py-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Materials & Packaging Tab -->
|
||||
<div class="tab-pane fade"
|
||||
t-att-class="state.activeCategory === 'Raw Materials & Packaging' ? 'show active' : ''"
|
||||
id="raw-materials-packaging"
|
||||
role="tabpanel">
|
||||
<!-- Raw Materials & Packaging Table -->
|
||||
<div class="card border-0 shadow-lg rounded-4 overflow-hidden" style="border: 2px solid #dee2e6;">
|
||||
<div class="card-header bg-gradient-success border-0 py-3">
|
||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between">
|
||||
<div class="d-flex align-items-center mb-2 mb-md-0">
|
||||
<div class="bg-opacity-20 rounded-3 p-2 me-3">
|
||||
<i class="fas fa-industry text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0 text-white fw-semibold">Raw Materials & Packaging</h5>
|
||||
<small class="text-white text-opacity-75">
|
||||
Production input materials
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-light btn-sm px-3 rounded-3 d-flex align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-download me-2"></i>Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToCSV">
|
||||
<i class="fas fa-file-csv me-2"></i>CSV
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToExcel">
|
||||
<i class="fas fa-file-excel me-2"></i>Excel
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click="exportToJSON">
|
||||
<i class="fas fa-file-code me-2"></i>JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm px-3 rounded-3 d-flex align-items-center"
|
||||
t-on-click="printTable">
|
||||
<i class="fas fa-print me-2"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Row -->
|
||||
<div class="table-filters p-3 border-bottom bg-light">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Product Code"
|
||||
t-model="state.filters.productCode"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Product Name"
|
||||
t-model="state.filters.productName"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Min Stock"
|
||||
t-model="state.filters.minStock"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Max Stock"
|
||||
t-model="state.filters.maxStock"
|
||||
t-on-input="onFilterKeyup"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
t-on-click="clearFilters">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
Showing <span t-esc="this.getFilteredRawMaterials().length"/> of <span t-esc="this.getRawMaterials().length"/> records
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
Total Value: <span t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.value || 0), 0))"/> ₹
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Table Container -->
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead class="table-light sticky-top" style="top: 0; z-index: 1;">
|
||||
<tr>
|
||||
<th style="min-width: 60px; text-align:center;">S.No</th>
|
||||
<th t-on-click="() => this.sortTable('product_code')"
|
||||
style="cursor: pointer; min-width: 100px;">
|
||||
Product Code
|
||||
<i t-att-class="this.getSortIcon('product_code')" class="ms-1"></i>
|
||||
</th>
|
||||
<th t-on-click="() => this.sortTable('product_name')"
|
||||
style="cursor: pointer; min-width: 300px;">
|
||||
Product Name
|
||||
<i t-att-class="this.getSortIcon('product_name')" class="ms-1"></i>
|
||||
</th>
|
||||
<th style="min-width: 120px; text-align: right;">Opening Stock</th>
|
||||
<th style="min-width: 120px; text-align: right;">Receipts</th>
|
||||
<th style="min-width: 120px; text-align: right;">Consumption</th>
|
||||
<th t-on-click="() => this.sortTable('closing_stock')"
|
||||
style="cursor: pointer; min-width: 120px; text-align: right;">
|
||||
Closing Stock
|
||||
<i t-att-class="this.getSortIcon('closing_stock')" class="ms-1"></i>
|
||||
</th>
|
||||
<th style="min-width: 80px;">UOM</th>
|
||||
<th style="min-width: 120px; text-align: right;">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="this.getFilteredRawMaterials()" t-as="row" t-key="row_index">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<t t-esc="row_index + 1"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-esc="row.product_code || 'N/A'"/>
|
||||
</td>
|
||||
<td t-esc="row.product_name || 'N/A'"/>
|
||||
<td class="text-end" t-esc="this.formatFloat(row.opening_stock)"/>
|
||||
<td class="text-end">
|
||||
<span class="text-success" t-esc="this.formatFloat(row.receipts)"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="text-warning" t-esc="this.formatFloat(row.consumption)"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-esc="this.formatFloat(row.closing_stock)"/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary px-2 py-1">
|
||||
<t t-esc="row.uom || 'N/A'"/>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<span class="text-success">
|
||||
<t t-esc="this.formatFloat(row.value)"/> ₹
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="this.getFilteredRawMaterials().length === 0">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="fas fa-database fa-2x mb-3"></i>
|
||||
<p class="mb-0">No raw materials available</p>
|
||||
<small>Try adjusting your filters or loading more data</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
<tfoot t-if="this.getFilteredRawMaterials().length > 0">
|
||||
<tr class="table-light">
|
||||
<td colspan="3" class="fw-bold">Totals</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.opening_stock || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.receipts || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.consumption || 0), 0))"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.closing_stock || 0), 0))"/>
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="text-end fw-bold text-success">
|
||||
<t t-esc="this.formatFloat(this.getFilteredRawMaterials().reduce((sum, row) => sum + (row.value || 0), 0))"/> ₹
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Padding -->
|
||||
<div class="card-footer bg-transparent border-0 py-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<style>
|
||||
/* Modern UI Enhancements */
|
||||
.bg-gradient-light {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
<!-- Total Products Card -->
|
||||
<div class="col mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="text-primary mb-3" style="font-size: 2rem;">
|
||||
📦
|
||||
</div>
|
||||
<h6 class="card-title text-muted small mb-2">Total Products</h6>
|
||||
<h4 class="text-primary fw-bold mb-0">
|
||||
<span t-esc="this.getTotalProductsCount()"/>
|
||||
</h4>
|
||||
<small class="text-muted" t-esc="'Across ' + this.getCategoryCount() + ' categories'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
}
|
||||
|
||||
<!-- Current Stock Value Card -->
|
||||
<div class="col mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="text-success mb-3" style="font-size: 2rem;">
|
||||
💰
|
||||
</div>
|
||||
<h6 class="card-title text-muted small mb-2">Current Stock Value</h6>
|
||||
<h4 class="text-success fw-bold mb-0" t-esc="this.getTotalStockValue()"/>
|
||||
<small class="text-muted">Total inventory value</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.bg-gradient-success {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
<!-- Out of Stock Products Card -->
|
||||
<div class="col mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="text-danger mb-3" style="font-size: 2rem;">
|
||||
❌
|
||||
</div>
|
||||
<h6 class="card-title text-muted small mb-2">Out of Stock</h6>
|
||||
<h4 class="text-danger fw-bold mb-0" t-esc="this.getOutOfStockCount()"/>
|
||||
<small class="text-muted">Products need restocking</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
<!-- Low Stock Alert Card -->
|
||||
<div class="col mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="text-info mb-3" style="font-size: 2rem;">
|
||||
⚠️
|
||||
</div>
|
||||
<h6 class="card-title text-muted small mb-2">Low Stock Alert</h6>
|
||||
<h4 class="text-info fw-bold mb-0" t-esc="this.getLowStockCount()"/>
|
||||
<small class="text-muted">Below minimum levels</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.hover-lift {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
</div>
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12) !important;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-cubes me-2"></i>Stock Data Grid
|
||||
<small class="float-end" t-esc="'Showing data from ' + (state.fromDate || 'start') + ' to ' + (state.toDate || 'end')"/>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.rounded-4 {
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
.rounded-3 {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Custom Tab Styling */
|
||||
.nav-tabs-custom {
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.nav-tabs-custom .nav-link {
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-tabs-custom .nav-link:hover {
|
||||
color: #4f46e5;
|
||||
background: rgba(79, 70, 229, 0.05);
|
||||
}
|
||||
|
||||
.nav-tabs-custom .nav-link.active {
|
||||
color: #4f46e5;
|
||||
background: white;
|
||||
border-bottom: 3px solid #4f46e5;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Table Styling */
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(79, 70, 229, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
font-size: 0.8rem !important;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
/* Table Filters Styling */
|
||||
.table-filters {
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.table-filters .form-control-sm {
|
||||
font-size: 0.8rem;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.table-filters .btn-sm {
|
||||
height: 32px;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Table Outer Border */
|
||||
.card.shadow-lg {
|
||||
border: 2px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.table-responsive::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%);
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Category colors */
|
||||
.bg-primary { background-color: #4f46e5 !important; }
|
||||
.bg-success { background-color: #10b981 !important; }
|
||||
.bg-warning { background-color: #f59e0b !important; }
|
||||
.bg-info { background-color: #06b6d4 !important; }
|
||||
.bg-secondary { background-color: #6b7280 !important; }
|
||||
.bg-danger { background-color: #ef4444 !important; }
|
||||
|
||||
/* Gradient backgrounds */
|
||||
.bg-gradient {
|
||||
background: linear-gradient(135deg, currentColor 0%, rgba(0,0,0,0.1) 100%) !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container-fluid {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.card-body.p-4 {
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-tabs-custom .nav-link {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.table-filters .row {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table-filters .col-md-3,
|
||||
.table-filters .col-md-2 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-lift {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Form controls */
|
||||
.form-control {
|
||||
border-color: #e2e8f0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.15);
|
||||
}
|
||||
|
||||
/* Sticky header */
|
||||
.sticky-top {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Table footer */
|
||||
tfoot tr {
|
||||
border-top: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Bottom padding for tables */
|
||||
.card-footer.bg-transparent {
|
||||
min-height: 40px;
|
||||
}
|
||||
</style>
|
||||
</t>
|
||||
</templates>
|
||||
Loading…
Reference in New Issue