Merge pull request #38362 from rohitwaghchaure/feat-visual-plant-floor

feat: visual plant floor
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 23650b6..079350b 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -955,6 +955,14 @@
 		if update_status:
 			self.db_set("status", self.status)
 
+		if self.status in ["Completed", "Work In Progress"]:
+			status = {
+				"Completed": "Off",
+				"Work In Progress": "Production",
+			}.get(self.status)
+
+			self.update_status_in_workstation(status)
+
 	def set_wip_warehouse(self):
 		if not self.wip_warehouse:
 			self.wip_warehouse = frappe.db.get_single_value(
@@ -1035,6 +1043,12 @@
 
 		return False
 
+	def update_status_in_workstation(self, status):
+		if not self.workstation:
+			return
+
+		frappe.db.set_value("Workstation", self.workstation, "status", status)
+
 
 @frappe.whitelist()
 def make_time_log(args):
diff --git a/erpnext/manufacturing/doctype/plant_floor/__init__.py b/erpnext/manufacturing/doctype/plant_floor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/__init__.py
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
new file mode 100644
index 0000000..67e5acd
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
@@ -0,0 +1,256 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Plant Floor", {
+	setup(frm) {
+		frm.trigger("setup_queries");
+	},
+
+	setup_queries(frm) {
+		frm.set_query("warehouse", (doc) => {
+			if (!doc.company) {
+				frappe.throw(__("Please select Company first"));
+			}
+
+			return {
+				filters: {
+					"is_group": 0,
+					"company": doc.company
+				}
+			}
+		});
+	},
+
+	refresh(frm) {
+		frm.trigger('prepare_stock_dashboard')
+		frm.trigger('prepare_workstation_dashboard')
+	},
+
+	prepare_workstation_dashboard(frm) {
+		let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper);
+		wrapper.empty();
+
+		frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({
+			wrapper: wrapper,
+			skip_filters: true,
+			plant_floor: frm.doc.name,
+		});
+	},
+
+	prepare_stock_dashboard(frm) {
+		if (!frm.doc.warehouse) {
+			return;
+		}
+
+		let wrapper = $(frm.fields_dict["stock_summary"].wrapper);
+		wrapper.empty();
+
+		frappe.visual_stock = new VisualStock({
+			wrapper: wrapper,
+			frm: frm,
+		});
+	},
+});
+
+
+class VisualStock {
+	constructor(opts) {
+		Object.assign(this, opts);
+		this.make();
+	}
+
+	make() {
+		this.prepare_filters();
+		this.prepare_stock_summary({
+			start:0
+		});
+	}
+
+	prepare_filters() {
+		this.wrapper.append(`
+			<div class="row">
+				<div class="col-sm-12 filter-section section-body">
+
+				</div>
+			</div>
+		`);
+
+		this.item_filter = frappe.ui.form.make_control({
+			df: {
+				fieldtype: "Link",
+				fieldname: "item_code",
+				placeholder: __("Item"),
+				options: "Item",
+				onchange: () => this.prepare_stock_summary({
+					start:0,
+					item_code: this.item_filter.value
+				})
+			},
+			parent: this.wrapper.find('.filter-section'),
+			render_input: true,
+		});
+
+		this.item_filter.$wrapper.addClass('form-column col-sm-3');
+		this.item_filter.$wrapper.find('.clearfix').hide();
+
+		this.item_group_filter = frappe.ui.form.make_control({
+			df: {
+				fieldtype: "Link",
+				fieldname: "item_group",
+				placeholder: __("Item Group"),
+				options: "Item Group",
+				change: () => this.prepare_stock_summary({
+					start:0,
+					item_group: this.item_group_filter.value
+				})
+			},
+			parent: this.wrapper.find('.filter-section'),
+			render_input: true,
+		});
+
+		this.item_group_filter.$wrapper.addClass('form-column col-sm-3');
+		this.item_group_filter.$wrapper.find('.clearfix').hide();
+	}
+
+	prepare_stock_summary(args) {
+		let {start, item_code, item_group} = args;
+
+		this.get_stock_summary(start, item_code, item_group).then(stock_summary => {
+			this.wrapper.find('.stock-summary-container').remove();
+			this.wrapper.append(`<div class="col-sm-12 stock-summary-container" style="margin-bottom:20px"></div>`);
+			this.stock_summary = stock_summary.message;
+			this.render_stock_summary();
+			this.bind_events();
+		});
+	}
+
+	async get_stock_summary(start, item_code, item_group) {
+		let stock_summary = await frappe.call({
+			method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary",
+			args: {
+				warehouse: this.frm.doc.warehouse,
+				start: start,
+				item_code: item_code,
+				item_group: item_group
+			}
+		});
+
+		return stock_summary;
+	}
+
+	render_stock_summary() {
+		let template = frappe.render_template("stock_summary_template", {
+			stock_summary: this.stock_summary
+		});
+
+		this.wrapper.find('.stock-summary-container').append(template);
+	}
+
+	bind_events() {
+		this.wrapper.find('.btn-add').click((e) => {
+			this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
+
+			this.make_stock_entry([
+				{
+					label: __("For Item"),
+					fieldname: "item_code",
+					fieldtype: "Data",
+					read_only: 1,
+					default: this.item_code
+				},
+				{
+					label: __("Quantity"),
+					fieldname: "qty",
+					fieldtype: "Float",
+					reqd: 1
+				}
+			], __("Add Stock"), "Material Receipt")
+		});
+
+		this.wrapper.find('.btn-move').click((e) => {
+			this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
+
+			this.make_stock_entry([
+				{
+					label: __("For Item"),
+					fieldname: "item_code",
+					fieldtype: "Data",
+					read_only: 1,
+					default: this.item_code
+				},
+				{
+					label: __("Quantity"),
+					fieldname: "qty",
+					fieldtype: "Float",
+					reqd: 1
+				},
+				{
+					label: __("To Warehouse"),
+					fieldname: "to_warehouse",
+					fieldtype: "Link",
+					options: "Warehouse",
+					reqd: 1,
+					get_query: () => {
+						return {
+							filters: {
+								"is_group": 0,
+								"company": this.frm.doc.company
+							}
+						}
+					}
+				}
+			], __("Move Stock"), "Material Transfer")
+		});
+	}
+
+	make_stock_entry(fields, title, stock_entry_type) {
+		frappe.prompt(fields,
+			(values) => {
+				this.values = values;
+				this.stock_entry_type = stock_entry_type;
+				this.update_values();
+
+				this.frm.call({
+					method: "make_stock_entry",
+					doc: this.frm.doc,
+					args: {
+						kwargs: this.values,
+					},
+					callback: (r) => {
+						if (!r.exc) {
+							var doc = frappe.model.sync(r.message);
+							frappe.set_route("Form", r.message.doctype, r.message.name);
+						}
+					}
+				})
+			}, __(title), __("Create")
+		);
+	}
+
+	update_values() {
+		if (!this.values.qty) {
+			frappe.throw(__("Quantity is required"));
+		}
+
+		let from_warehouse = "";
+		let to_warehouse = "";
+
+		if (this.stock_entry_type == "Material Receipt") {
+			to_warehouse = this.frm.doc.warehouse;
+		} else {
+			from_warehouse = this.frm.doc.warehouse;
+			to_warehouse = this.values.to_warehouse;
+		}
+
+		this.values = {
+			...this.values,
+			...{
+				"company": this.frm.doc.company,
+				"item_code": this.item_code,
+				"from_warehouse": from_warehouse,
+				"to_warehouse": to_warehouse,
+				"purpose": this.stock_entry_type,
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
new file mode 100644
index 0000000..be0052c
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:floor_name",
+ "creation": "2023-10-06 15:06:07.976066",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "workstations_tab",
+  "plant_dashboard",
+  "stock_summary_tab",
+  "stock_summary",
+  "details_tab",
+  "column_break_mvbx",
+  "floor_name",
+  "company",
+  "warehouse"
+ ],
+ "fields": [
+  {
+   "fieldname": "floor_name",
+   "fieldtype": "Data",
+   "label": "Floor Name",
+   "unique": 1
+  },
+  {
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "workstations_tab",
+   "fieldtype": "Tab Break",
+   "label": "Workstations"
+  },
+  {
+   "fieldname": "plant_dashboard",
+   "fieldtype": "HTML",
+   "label": "Plant Dashboard"
+  },
+  {
+   "fieldname": "details_tab",
+   "fieldtype": "Tab Break",
+   "label": "Floor"
+  },
+  {
+   "fieldname": "column_break_mvbx",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "label": "Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "depends_on": "eval:!doc.__islocal && doc.warehouse",
+   "fieldname": "stock_summary_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Summary"
+  },
+  {
+   "fieldname": "stock_summary",
+   "fieldtype": "HTML",
+   "label": "Stock Summary"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-01-30 11:59:07.508535",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Plant Floor",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
new file mode 100644
index 0000000..d30b7d1
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.query_builder import Order
+from frappe.utils import get_link_to_form, nowdate, nowtime
+
+
+class PlantFloor(Document):
+	# begin: auto-generated types
+	# This code is auto-generated. Do not modify anything in this block.
+
+	from typing import TYPE_CHECKING
+
+	if TYPE_CHECKING:
+		from frappe.types import DF
+
+		company: DF.Link | None
+		floor_name: DF.Data | None
+		warehouse: DF.Link | None
+	# end: auto-generated types
+
+	@frappe.whitelist()
+	def make_stock_entry(self, kwargs):
+		if isinstance(kwargs, str):
+			kwargs = frappe.parse_json(kwargs)
+
+		if isinstance(kwargs, dict):
+			kwargs = frappe._dict(kwargs)
+
+		stock_entry = frappe.new_doc("Stock Entry")
+		stock_entry.update(
+			{
+				"company": kwargs.company,
+				"from_warehouse": kwargs.from_warehouse,
+				"to_warehouse": kwargs.to_warehouse,
+				"purpose": kwargs.purpose,
+				"stock_entry_type": kwargs.purpose,
+				"posting_date": nowdate(),
+				"posting_time": nowtime(),
+				"items": self.get_item_details(kwargs),
+			}
+		)
+
+		stock_entry.set_missing_values()
+
+		return stock_entry
+
+	def get_item_details(self, kwargs) -> list[dict]:
+		item_details = frappe.db.get_value(
+			"Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True
+		)
+		item_details.update(
+			{
+				"qty": kwargs.qty,
+				"uom": item_details.stock_uom,
+				"item_code": kwargs.item_code,
+				"conversion_factor": 1,
+				"s_warehouse": kwargs.from_warehouse,
+				"t_warehouse": kwargs.to_warehouse,
+			}
+		)
+
+		return [item_details]
+
+
+@frappe.whitelist()
+def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
+	stock_details = get_stock_details(
+		warehouse, start=start, item_code=item_code, item_group=item_group
+	)
+
+	max_count = 0.0
+	for d in stock_details:
+		d.actual_or_pending = (
+			d.projected_qty
+			+ d.reserved_qty
+			+ d.reserved_qty_for_production
+			+ d.reserved_qty_for_sub_contract
+		)
+		d.pending_qty = 0
+		d.total_reserved = (
+			d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract
+		)
+		if d.actual_or_pending > d.actual_qty:
+			d.pending_qty = d.actual_or_pending - d.actual_qty
+
+		d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count)
+		max_count = d.max_count
+		d.item_link = get_link_to_form("Item", d.item_code)
+
+	return stock_details
+
+
+def get_stock_details(warehouse, start=0, item_code=None, item_group=None):
+	item_table = frappe.qb.DocType("Item")
+	bin_table = frappe.qb.DocType("Bin")
+
+	query = (
+		frappe.qb.from_(bin_table)
+		.inner_join(item_table)
+		.on(bin_table.item_code == item_table.name)
+		.select(
+			bin_table.item_code,
+			bin_table.actual_qty,
+			bin_table.projected_qty,
+			bin_table.reserved_qty,
+			bin_table.reserved_qty_for_production,
+			bin_table.reserved_qty_for_sub_contract,
+			bin_table.reserved_qty_for_production_plan,
+			bin_table.reserved_stock,
+			item_table.item_name,
+			item_table.item_group,
+			item_table.image,
+		)
+		.where(bin_table.warehouse == warehouse)
+		.limit(20)
+		.offset(start)
+		.orderby(bin_table.actual_qty, order=Order.desc)
+	)
+
+	if item_code:
+		query = query.where(bin_table.item_code == item_code)
+
+	if item_group:
+		query = query.where(item_table.item_group == item_group)
+
+	return query.run(as_dict=True)
diff --git a/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
new file mode 100644
index 0000000..8824c98
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
@@ -0,0 +1,61 @@
+{% $.each(stock_summary, (idx, row) => { %}
+<div class="row" style="border-bottom:1px solid var(--border-color); padding:4px 5px; margin-top: 3px;margin-bottom: 3px;">
+	<div class="col-sm-1">
+		{% if(row.image) { %}
+			<img style="width:50px;height:50px;" src="{{row.image}}">
+		{% } else { %}
+			<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(row.item_code, 2)}}</div>
+		{% } %}
+	</div>
+	<div class="col-sm-3">
+		{% if (row.item_code === row.item_name) { %}
+			{{row.item_link}}
+		{% } else { %}
+			{{row.item_link}}
+			<p>
+				{{row.item_name}}
+			</p>
+		{% } %}
+
+	</div>
+	<div class="col-sm-1" title="{{ __('Actual Qty') }}">
+		{{ frappe.format(row.actual_qty, { fieldtype: "Float"})}}
+	</div>
+	<div class="col-sm-1" title="{{ __('Reserved Stock') }}">
+		{{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}}
+	</div>
+	<div class="col-sm-4 small">
+		<span class="inline-graph">
+			<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
+				<span class="inline-graph-count">{{ row.total_reserved }}</span>
+				<span class="inline-graph-bar">
+					<span class="inline-graph-bar-inner"
+						style="width: {{ cint(Math.abs(row.total_reserved)/row.max_count * 100) || 5 }}%">
+					</span>
+				</span>
+			</span>
+			<span class="inline-graph-half" title="{{ __("Actual Qty {0} / Waiting Qty {1}", [row.actual_qty, row.pending_qty]) }}">
+				<span class="inline-graph-count">
+					{{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }}
+				</span>
+				<span class="inline-graph-bar">
+					<span class="inline-graph-bar-inner dark"
+						style="width: {{ cint(row.actual_qty/row.max_count * 100) }}%">
+					</span>
+					{% if row.pending_qty > 0 %}
+					<span class="inline-graph-bar-inner" title="{{ __("Projected Qty") }}"
+						style="width: {{ cint(row.pending_qty/row.max_count * 100) }}%">
+					</span>
+					{% endif %}
+				</span>
+			</span>
+		</span>
+	</div>
+	<div class="col-sm-1">
+		<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">Add</button>
+	</div>
+	<div class="col-sm-1">
+		<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">Move</button>
+	</div>
+</div>
+{% }); %}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py
new file mode 100644
index 0000000..2fac211
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestPlantFloor(FrappeTestCase):
+	pass
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js
index f830b17..e3ad3fe 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation.js
@@ -2,6 +2,28 @@
 // License: GNU General Public License v3. See license.txt
 
 frappe.ui.form.on("Workstation", {
+	set_illustration_image(frm) {
+		let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image;
+		if (status_image_field) {
+			frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field);
+		}
+	},
+
+	refresh(frm) {
+		frm.trigger("set_illustration_image");
+		frm.trigger("prepapre_dashboard");
+	},
+
+	prepapre_dashboard(frm) {
+		let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper);
+		$parent.empty();
+
+		let workstation_dashboard = new WorkstationDashboard({
+			wrapper: $parent,
+			frm: frm
+		});
+	},
+
 	onload(frm) {
 		if(frm.is_new())
 		{
@@ -54,3 +76,243 @@
 
 
 ];
+
+
+class WorkstationDashboard {
+	constructor({ wrapper, frm }) {
+		this.$wrapper = $(wrapper);
+		this.frm = frm;
+
+		this.prepapre_dashboard();
+	}
+
+	prepapre_dashboard() {
+		frappe.call({
+			method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards",
+			args: {
+				workstation: this.frm.doc.name
+			},
+			callback: (r) => {
+				if (r.message) {
+					this.job_cards = r.message;
+					this.render_job_cards();
+				}
+			}
+		});
+	}
+
+	render_job_cards() {
+		let template  = frappe.render_template("workstation_job_card", {
+			data: this.job_cards
+		});
+
+		this.$wrapper.html(template);
+		this.prepare_timer();
+		this.toggle_job_card();
+		this.bind_events();
+	}
+
+	toggle_job_card() {
+		this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
+			$(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide")
+			if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide"))
+				$(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1"))
+			else
+				$(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1"))
+		});
+	}
+
+	bind_events() {
+		this.$wrapper.find(".make-material-request").on("click", (e) => {
+			let job_card = $(e.currentTarget).attr("job-card");
+			this.make_material_request(job_card);
+		});
+
+		this.$wrapper.find(".btn-start").on("click", (e) => {
+			let job_card = $(e.currentTarget).attr("job-card");
+			this.start_job(job_card);
+		});
+
+		this.$wrapper.find(".btn-complete").on("click", (e) => {
+			let job_card = $(e.currentTarget).attr("job-card");
+			let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
+			this.complete_job(job_card, pending_qty);
+		});
+	}
+
+	start_job(job_card) {
+		let me = this;
+		frappe.prompt([
+			{
+				fieldtype: 'Datetime',
+				label: __('Start Time'),
+				fieldname: 'start_time',
+				reqd: 1,
+				default: frappe.datetime.now_datetime()
+			},
+			{
+				label: __('Operator'),
+				fieldname: 'employee',
+				fieldtype: 'Link',
+				options: 'Employee',
+			}
+		], data => {
+			this.frm.call({
+				method: "start_job",
+				doc: this.frm.doc,
+				args: {
+					job_card: job_card,
+					from_time: data.start_time,
+					employee: data.employee,
+				},
+				callback(r) {
+					if (r.message) {
+						me.job_cards = [r.message];
+						me.prepare_timer()
+						me.update_job_card_details();
+					}
+				}
+			});
+		}, __("Enter Value"), __("Start Job"));
+	}
+
+	complete_job(job_card, qty_to_manufacture) {
+		let me = this;
+		let fields = [
+			{
+				fieldtype: 'Float',
+				label: __('Completed Quantity'),
+				fieldname: 'qty',
+				reqd: 1,
+				default: flt(qty_to_manufacture || 0)
+			},
+			{
+				fieldtype: 'Datetime',
+				label: __('End Time'),
+				fieldname: 'end_time',
+				default: frappe.datetime.now_datetime()
+			},
+		];
+
+		frappe.prompt(fields, data => {
+			if (data.qty <= 0) {
+				frappe.throw(__("Quantity should be greater than 0"));
+			}
+
+			this.frm.call({
+				method: "complete_job",
+				doc: this.frm.doc,
+				args: {
+					job_card: job_card,
+					qty: data.qty,
+					to_time: data.end_time,
+				},
+				callback: function(r) {
+					if (r.message) {
+						me.job_cards = [r.message];
+						me.prepare_timer()
+						me.update_job_card_details();
+					}
+				}
+			});
+		}, __("Enter Value"), __("Submit"));
+	}
+
+	make_material_request(job_card) {
+		frappe.call({
+			method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
+			args: {
+				source_name: job_card,
+			},
+			callback: (r) => {
+				if (r.message) {
+					var doc = frappe.model.sync(r.message)[0];
+					frappe.set_route("Form", doc.doctype, doc.name);
+				}
+			}
+		});
+	}
+
+	prepare_timer() {
+		this.job_cards.forEach((data) => {
+			if (data.time_logs?.length) {
+				data._current_time = this.get_current_time(data);
+				if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
+					this.updateStopwatch(data);
+				} else {
+					this.initialiseTimer(data);
+				}
+			}
+		});
+	}
+
+	update_job_card_details() {
+		let color_map = {
+			"Pending": "var(--bg-blue)",
+			"In Process": "var(--bg-yellow)",
+			"Submitted": "var(--bg-blue)",
+			"Open": "var(--bg-gray)",
+			"Closed": "var(--bg-green)",
+			"Work In Progress": "var(--bg-orange)",
+		}
+
+		this.job_cards.forEach((data) => {
+			let job_card_selector =  this.$wrapper.find(`
+				[data-name='${data.name}']`
+			);
+
+			$(job_card_selector).find(".job-card-status").text(data.status);
+			$(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
+
+			if (data.status === "Work In Progress") {
+				$(job_card_selector).find(".btn-start").addClass("hide");
+				$(job_card_selector).find(".btn-complete").removeClass("hide");
+			} else if (data.status === "Completed") {
+				$(job_card_selector).find(".btn-start").addClass("hide");
+				$(job_card_selector).find(".btn-complete").addClass("hide");
+			}
+		});
+	}
+
+	initialiseTimer(data) {
+		setInterval(() => {
+			data._current_time += 1;
+			this.updateStopwatch(data);
+		}, 1000);
+	}
+
+	updateStopwatch(data) {
+		let increment = data._current_time;
+		let hours = Math.floor(increment / 3600);
+		let minutes = Math.floor((increment - (hours * 3600)) / 60);
+		let seconds = cint(increment - (hours * 3600) - (minutes * 60));
+
+		let job_card_selector = `[data-job-card='${data.name}']`
+		let timer_selector =  this.$wrapper.find(job_card_selector)
+
+		$(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
+		$(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
+		$(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
+	}
+
+	get_current_time(data) {
+		let current_time = 0.0;
+		data.time_logs.forEach(d => {
+			if (d.to_time) {
+				if (d.time_in_mins) {
+					current_time += flt(d.time_in_mins, 2) * 60;
+				} else {
+					current_time += this.get_seconds_diff(d.to_time, d.from_time);
+				}
+			} else {
+				current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
+			}
+		});
+
+		return current_time;
+	}
+
+	get_seconds_diff(d1, d2) {
+		return moment(d1).diff(d2, "seconds");
+	}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json
index 881cba0..5912714 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.json
+++ b/erpnext/manufacturing/doctype/workstation/workstation.json
@@ -8,10 +8,24 @@
  "document_type": "Setup",
  "engine": "InnoDB",
  "field_order": [
+  "dashboard_tab",
+  "workstation_dashboard",
+  "details_tab",
   "workstation_name",
-  "production_capacity",
-  "column_break_3",
   "workstation_type",
+  "plant_floor",
+  "column_break_3",
+  "production_capacity",
+  "warehouse",
+  "production_capacity_section",
+  "parts_per_hour",
+  "workstation_status_tab",
+  "status",
+  "column_break_glcv",
+  "illustration_section",
+  "on_status_image",
+  "column_break_etmc",
+  "off_status_image",
   "over_heads",
   "hour_rate_electricity",
   "hour_rate_consumable",
@@ -24,7 +38,9 @@
   "description",
   "working_hours_section",
   "holiday_list",
-  "working_hours"
+  "working_hours",
+  "total_working_hours",
+  "connections_tab"
  ],
  "fields": [
   {
@@ -120,9 +136,10 @@
   },
   {
    "default": "1",
+   "description": "Run parallel job cards in a workstation",
    "fieldname": "production_capacity",
    "fieldtype": "Int",
-   "label": "Production Capacity",
+   "label": "Job Capacity",
    "reqd": 1
   },
   {
@@ -145,12 +162,97 @@
   {
    "fieldname": "section_break_11",
    "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "plant_floor",
+   "fieldtype": "Link",
+   "label": "Plant Floor",
+   "options": "Plant Floor"
+  },
+  {
+   "fieldname": "workstation_status_tab",
+   "fieldtype": "Tab Break",
+   "label": "Workstation Status"
+  },
+  {
+   "fieldname": "illustration_section",
+   "fieldtype": "Section Break",
+   "label": "Status Illustration"
+  },
+  {
+   "fieldname": "column_break_etmc",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Status",
+   "options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup"
+  },
+  {
+   "fieldname": "column_break_glcv",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "on_status_image",
+   "fieldtype": "Attach Image",
+   "label": "Active Status"
+  },
+  {
+   "fieldname": "off_status_image",
+   "fieldtype": "Attach Image",
+   "label": "Inactive Status"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "label": "Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "fieldname": "production_capacity_section",
+   "fieldtype": "Section Break",
+   "label": "Production Capacity"
+  },
+  {
+   "fieldname": "parts_per_hour",
+   "fieldtype": "Float",
+   "label": "Parts Per Hour"
+  },
+  {
+   "fieldname": "total_working_hours",
+   "fieldtype": "Float",
+   "label": "Total Working Hours"
+  },
+  {
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "dashboard_tab",
+   "fieldtype": "Tab Break",
+   "label": "Job Cards"
+  },
+  {
+   "fieldname": "details_tab",
+   "fieldtype": "Tab Break",
+   "label": "Details"
+  },
+  {
+   "fieldname": "connections_tab",
+   "fieldtype": "Tab Break",
+   "label": "Connections",
+   "show_dashboard": 1
+  },
+  {
+   "fieldname": "workstation_dashboard",
+   "fieldtype": "HTML",
+   "label": "Workstation Dashboard"
   }
  ],
  "icon": "icon-wrench",
  "idx": 1,
+ "image_field": "on_status_image",
  "links": [],
- "modified": "2022-11-04 17:39:01.549346",
+ "modified": "2023-11-30 12:43:35.808845",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Workstation",
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 0f05eaa..3d40a2d 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -11,7 +11,11 @@
 	comma_and,
 	flt,
 	formatdate,
+	get_link_to_form,
+	get_time,
+	get_url_to_form,
 	getdate,
+	time_diff_in_hours,
 	time_diff_in_seconds,
 	to_timedelta,
 )
@@ -60,6 +64,23 @@
 	def before_save(self):
 		self.set_data_based_on_workstation_type()
 		self.set_hour_rate()
+		self.set_total_working_hours()
+
+	def set_total_working_hours(self):
+		self.total_working_hours = 0.0
+		for row in self.working_hours:
+			self.validate_working_hours(row)
+
+			if row.start_time and row.end_time:
+				row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours"))
+				self.total_working_hours += row.hours
+
+	def validate_working_hours(self, row):
+		if not (row.start_time and row.end_time):
+			frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
+
+		if get_time(row.start_time) >= get_time(row.end_time):
+			frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
 
 	def set_hour_rate(self):
 		self.hour_rate = (
@@ -143,6 +164,141 @@
 
 		return schedule_date
 
+	@frappe.whitelist()
+	def start_job(self, job_card, from_time, employee):
+		doc = frappe.get_doc("Job Card", job_card)
+		doc.append("time_logs", {"from_time": from_time, "employee": employee})
+		doc.save(ignore_permissions=True)
+
+		return doc
+
+	@frappe.whitelist()
+	def complete_job(self, job_card, qty, to_time):
+		doc = frappe.get_doc("Job Card", job_card)
+		for row in doc.time_logs:
+			if not row.to_time:
+				row.to_time = to_time
+				row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
+				row.completed_qty = qty
+
+		doc.save(ignore_permissions=True)
+		doc.submit()
+
+		return doc
+
+
+@frappe.whitelist()
+def get_job_cards(workstation):
+	if frappe.has_permission("Job Card", "read"):
+		jc_data = frappe.get_all(
+			"Job Card",
+			fields=[
+				"name",
+				"production_item",
+				"work_order",
+				"operation",
+				"total_completed_qty",
+				"for_quantity",
+				"transferred_qty",
+				"status",
+				"expected_start_date",
+				"expected_end_date",
+				"time_required",
+				"wip_warehouse",
+			],
+			filters={
+				"workstation": workstation,
+				"docstatus": ("<", 2),
+				"status": ["not in", ["Completed", "Stopped"]],
+			},
+			order_by="expected_start_date, expected_end_date",
+		)
+
+		job_cards = [row.name for row in jc_data]
+		raw_materials = get_raw_materials(job_cards)
+		time_logs = get_time_logs(job_cards)
+
+		allow_excess_transfer = frappe.db.get_single_value(
+			"Manufacturing Settings", "job_card_excess_transfer"
+		)
+
+		for row in jc_data:
+			row.progress_percent = (
+				flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
+			)
+			row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
+			row.status_color = get_status_color(row.status)
+			row.job_card_link = get_link_to_form("Job Card", row.name)
+			row.work_order_link = get_link_to_form("Work Order", row.work_order)
+
+			row.raw_materials = raw_materials.get(row.name, [])
+			row.time_logs = time_logs.get(row.name, [])
+			row.make_material_request = False
+			if row.for_quantity > row.transferred_qty or allow_excess_transfer:
+				row.make_material_request = True
+
+		return jc_data
+
+
+def get_status_color(status):
+	color_map = {
+		"Pending": "var(--bg-blue)",
+		"In Process": "var(--bg-yellow)",
+		"Submitted": "var(--bg-blue)",
+		"Open": "var(--bg-gray)",
+		"Closed": "var(--bg-green)",
+		"Work In Progress": "var(--bg-orange)",
+	}
+
+	return color_map.get(status, "var(--bg-blue)")
+
+
+def get_raw_materials(job_cards):
+	raw_materials = {}
+
+	data = frappe.get_all(
+		"Job Card Item",
+		fields=[
+			"parent",
+			"item_code",
+			"item_group",
+			"uom",
+			"item_name",
+			"source_warehouse",
+			"required_qty",
+			"transferred_qty",
+		],
+		filters={"parent": ["in", job_cards]},
+	)
+
+	for row in data:
+		raw_materials.setdefault(row.parent, []).append(row)
+
+	return raw_materials
+
+
+def get_time_logs(job_cards):
+	time_logs = {}
+
+	data = frappe.get_all(
+		"Job Card Time Log",
+		fields=[
+			"parent",
+			"name",
+			"employee",
+			"from_time",
+			"to_time",
+			"time_in_mins",
+		],
+		filters={"parent": ["in", job_cards], "parentfield": "time_logs"},
+		order_by="parent, idx",
+	)
+
+	for row in data:
+		time_logs.setdefault(row.parent, []).append(row)
+
+	return time_logs
+
 
 @frappe.whitelist()
 def get_default_holiday_list():
@@ -201,3 +357,52 @@
 				+ "\n".join(applicable_holidays),
 				WorkstationHolidayError,
 			)
+
+
+@frappe.whitelist()
+def get_workstations(**kwargs):
+	kwargs = frappe._dict(kwargs)
+	_workstation = frappe.qb.DocType("Workstation")
+
+	query = (
+		frappe.qb.from_(_workstation)
+		.select(
+			_workstation.name,
+			_workstation.description,
+			_workstation.status,
+			_workstation.on_status_image,
+			_workstation.off_status_image,
+		)
+		.orderby(_workstation.workstation_type, _workstation.name)
+		.where(_workstation.plant_floor == kwargs.plant_floor)
+	)
+
+	if kwargs.workstation:
+		query = query.where(_workstation.name == kwargs.workstation)
+
+	if kwargs.workstation_type:
+		query = query.where(_workstation.workstation_type == kwargs.workstation_type)
+
+	if kwargs.workstation_status:
+		query = query.where(_workstation.status == kwargs.workstation_status)
+
+	data = query.run(as_dict=True)
+
+	color_map = {
+		"Production": "var(--green-600)",
+		"Off": "var(--gray-600)",
+		"Idle": "var(--gray-600)",
+		"Problem": "var(--red-600)",
+		"Maintenance": "var(--yellow-600)",
+		"Setup": "var(--blue-600)",
+	}
+
+	for d in data:
+		d.workstation_name = get_link_to_form("Workstation", d.name)
+		d.status_image = d.on_status_image
+		d.background_color = color_map.get(d.status, "var(--red-600)")
+		d.workstation_link = get_url_to_form("Workstation", d.name)
+		if d.status != "Production":
+			d.status_image = d.off_status_image
+
+	return data
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
new file mode 100644
index 0000000..9770785
--- /dev/null
+++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
@@ -0,0 +1,125 @@
+<style>
+	.job-card-link {
+		min-height: 100px;
+	}
+
+	.section-head-job-card {
+		margin-bottom: 0px;
+		padding-bottom: 0px;
+	}
+</style>
+
+<div style = "max-height: 400px; overflow-y: auto;">
+{% $.each(data, (idx, d) => { %}
+	<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
+		<div class="section-head section-head-job-card">
+			{{ d.operation }} - {{ d.production_item }}
+			<span class="ml-2 collapse-indicator-job mb-1" style="">
+				{{frappe.utils.icon("es-line-down", "sm", "mb-1")}}
+			</span>
+		</div>
+		<div class="row form-section" style="width:100%;margin-bottom:10px">
+			<div class="form-column col-sm-3">
+				<div class="frappe-control" title="{{__('Job Card')}}" style="text-decoration:underline">
+					{{ d.job_card_link }}
+				</div>
+				<div class="frappe-control" title="{{__('Work Order')}}" style="text-decoration:underline">
+					{{ d.work_order_link }}
+				</div>
+			</div>
+			<div class="form-column col-sm-2">
+				<div class="frappe-control timer" title="{{__('Timer')}}" style="text-align:center;font-size:14px;" data-job-card = {{escape(d.name)}}>
+					<span class="hours">00</span>
+					<span class="colon">:</span>
+					<span class="minutes">00</span>
+					<span class="colon">:</span>
+					<span class="seconds">00</span>
+				</div>
+
+				{% if(d.status === "Open") { %}
+					<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
+						{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
+					</div>
+				{% } else { %}
+					<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
+						{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
+					</div>
+				{% } %}
+
+			</div>
+			<div class="form-column col-sm-2">
+				<div class="frappe-control job-card-status" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
+					{{ d.status }}
+				</div>
+			</div>
+			<div class="form-column col-sm-2">
+				<div class="frappe-control" title="{{__('Qty to Manufacture')}}">
+					<div class="progress" title = "{{d.progress_title}}">
+						<div class="progress-bar progress-bar-success" style="width: {{d.progress_percent}}%">
+						</div>
+					</div>
+				</div>
+				<div class="frappe-control" style="text-align: center; font-size: 10px;">
+					{{ d.for_quantity }} / {{ d.total_completed_qty }}
+				</div>
+			</div>
+			<div class="form-column col-sm-2 text-center">
+				<button style="width: 85px;" class="btn btn-default btn-start {% if(d.status !== "Open") { %} hide {% } %}" job-card="{{d.name}}"> {{__("Start")}} </button>
+				<button style="width: 85px;" class="btn btn-default btn-complete {% if(d.status === "Open") { %} hide {% } %}" job-card="{{d.name}}" pending-qty="{{d.for_quantity - d.transferred_qty}}"> {{__("Complete")}} </button>
+			</div>
+		</div>
+
+		<div class="section-body section-body-job-card form-section hide">
+			<hr>
+			<div class="row">
+				<div class="form-column col-sm-2">
+					{{ __("Raw Materials") }}
+				</div>
+				{% if(d.make_material_request) { %}
+					<div class="form-column col-sm-10 text-right">
+						<button class="btn btn-default btn-xs make-material-request" job-card="{{d.name}}">{{ __("Material Request") }}</button>
+					</div>
+				{% } %}
+			</div>
+
+			{% if(d.raw_materials) { %}
+			<table class="table table-bordered table-condensed">
+				<thead>
+					<tr>
+						<th style="width: 5%" class="table-sr">Sr</th>
+
+						<th style="width: 15%">{{ __("Item") }}</th>
+						<th style="width: 15%">{{ __("Warehouse") }}</th>
+						<th style="width: 10%">{{__("UOM")}}</th>
+						<th style="width: 15%">{{__("Item Group")}}</th>
+						<th style="width: 20%" >{{__("Required Qty")}}</th>
+						<th style="width: 20%" >{{__("Transferred Qty")}}</th>
+					</tr>
+				</thead>
+				<tbody>
+
+				{% $.each(d.raw_materials, (row_index, child_row) => { %}
+					<tr>
+						<td class="table-sr">{{ row_index+1 }}</td>
+						{% if(child_row.item_code === child_row.item_name) { %}
+							<td>{{ child_row.item_code }}</td>
+						{% } else { %}
+							<td>{{ child_row.item_code }}: {{child_row.item_name}}</td>
+						{% } %}
+						<td>{{ child_row.source_warehouse }}</td>
+						<td>{{ child_row.uom }}</td>
+						<td>{{ child_row.item_group }}</td>
+						<td>{{ child_row.required_qty }}</td>
+						<td>{{ child_row.transferred_qty }}</td>
+					</tr>
+				{% }); %}
+
+				</tbody>
+			{% } %}
+
+			</table>
+		</div>
+
+	</div>
+{% }); %}
+</div>
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js
index 61f2062..86928ca 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_list.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js
@@ -1,5 +1,16 @@
 
 frappe.listview_settings['Workstation'] = {
-	// add_fields: ["status"],
-	// filters:[["status","=", "Open"]]
+	add_fields: ["status"],
+	get_indicator: function(doc) {
+		let color_map = {
+			"Production": "green",
+			"Off": "gray",
+			"Idle": "gray",
+			"Problem": "red",
+			"Maintenance": "yellow",
+			"Setup": "blue",
+		}
+
+		return [__(doc.status), color_map[doc.status], true];
+	}
 };
diff --git a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
index a79182f..b185f7d 100644
--- a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
+++ b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
@@ -1,150 +1,58 @@
 {
- "allow_copy": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "beta": 0, 
- "creation": "2014-12-24 14:46:40.678236", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 1, 
- "engine": "InnoDB", 
+ "actions": [],
+ "creation": "2014-12-24 14:46:40.678236",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "start_time",
+  "hours",
+  "column_break_2",
+  "end_time",
+  "enabled"
+ ],
  "fields": [
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "start_time", 
-   "fieldtype": "Time", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Start Time", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "start_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "Start Time",
+   "reqd": 1
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "column_break_2", 
-   "fieldtype": "Column Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "end_time", 
-   "fieldtype": "Time", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "End Time", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "end_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "End Time",
+   "reqd": 1
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "default": "1", 
-   "fieldname": "enabled", 
-   "fieldtype": "Check", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Enabled", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "default": "1",
+   "fieldname": "enabled",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Enabled"
+  },
+  {
+   "fieldname": "hours",
+   "fieldtype": "Float",
+   "label": "Hours",
+   "read_only": 1
   }
- ], 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
-
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 1, 
- "max_attachments": 0, 
- "modified": "2016-12-13 05:02:36.754145", 
- "modified_by": "Administrator", 
- "module": "Manufacturing", 
- "name": "Workstation Working Hour", 
- "name_case": "", 
- "owner": "Administrator", 
- "permissions": [], 
- "quick_entry": 0, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-10-25 14:48:29.697498",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Workstation Working Hour",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/page/visual_plant_floor/__init__.py b/erpnext/manufacturing/page/visual_plant_floor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/page/visual_plant_floor/__init__.py
diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js
new file mode 100644
index 0000000..38667e8
--- /dev/null
+++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js
@@ -0,0 +1,13 @@
+
+
+frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) {
+	var page = frappe.ui.make_app_page({
+		parent: wrapper,
+		title: 'Visual Plant Floor',
+		single_column: true
+	});
+
+	frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor(
+		{wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page
+	);
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json
new file mode 100644
index 0000000..a907e97
--- /dev/null
+++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json
@@ -0,0 +1,29 @@
+{
+ "content": null,
+ "creation": "2023-10-06 15:17:39.215300",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2023-10-06 15:18:00.622073",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "visual-plant-floor",
+ "owner": "Administrator",
+ "page_name": "visual-plant-floor",
+ "roles": [
+  {
+   "role": "Manufacturing User"
+  },
+  {
+   "role": "Manufacturing Manager"
+  },
+  {
+   "role": "Operator"
+  }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Visual Plant Floor"
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 8e07850..d2520d6 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
 {
  "charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
  "creation": "2020-03-02 17:11:37.032604",
  "custom_blocks": [],
  "docstatus": 0,
@@ -316,7 +316,7 @@
    "type": "Link"
   }
  ],
- "modified": "2023-08-08 22:28:39.633891",
+ "modified": "2024-01-30 21:49:58.577218",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Manufacturing",
@@ -339,6 +339,13 @@
   {
    "color": "Grey",
    "doc_view": "List",
+   "label": "Plant Floor",
+   "link_to": "Plant Floor",
+   "type": "DocType"
+  },
+  {
+   "color": "Grey",
+   "doc_view": "List",
    "label": "BOM Creator",
    "link_to": "BOM Creator",
    "type": "DocType"
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index dee9a06..b847e57 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -5,6 +5,8 @@
 import "./utils/party";
 import "./controllers/stock_controller";
 import "./payment/payments";
+import "./templates/visual_plant_floor_template.html";
+import "./plant_floor_visual/visual_plant";
 import "./controllers/taxes_and_totals";
 import "./controllers/transaction";
 import "./templates/item_selector.html";
diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js
new file mode 100644
index 0000000..8cd73ad
--- /dev/null
+++ b/erpnext/public/js/plant_floor_visual/visual_plant.js
@@ -0,0 +1,157 @@
+class VisualPlantFloor {
+	constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) {
+		this.wrapper = wrapper;
+		this.plant_floor = plant_floor;
+		this.skip_filters = skip_filters;
+
+		this.make();
+		if (!this.skip_filters) {
+			this.page = page;
+			this.add_filter();
+			this.prepare_menu();
+		}
+	}
+
+	make() {
+		this.wrapper.append(`
+			<div class="plant-floor">
+				<div class="plant-floor-filter">
+				</div>
+				<div class="plant-floor-container col-sm-12">
+				</div>
+			</div>
+		`);
+
+		if (!this.skip_filters) {
+			this.filter_wrapper = this.wrapper.find('.plant-floor-filter');
+			this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization');
+		} else if(this.plant_floor) {
+			this.wrapper.find('.plant-floor').css('border', 'none');
+			this.prepare_data();
+		}
+	}
+
+	prepare_data() {
+		frappe.call({
+			method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
+			args: {
+				plant_floor: this.plant_floor,
+			},
+			callback: (r) => {
+				this.workstations = r.message;
+				this.render_workstations();
+			}
+		});
+	}
+
+	add_filter() {
+		this.plant_floor = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Link',
+				options: 'Plant Floor',
+				fieldname: 'plant_floor',
+				label: __('Plant Floor'),
+				reqd: 1,
+				onchange: () => {
+					this.render_plant_visualization();
+				}
+			},
+			parent: this.filter_wrapper,
+			render_input: true,
+		});
+
+		this.plant_floor.$wrapper.addClass('form-column col-sm-2');
+
+		this.workstation_type = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Link',
+				options: 'Workstation Type',
+				fieldname: 'workstation_type',
+				label: __('Machine Type'),
+				onchange: () => {
+					this.render_plant_visualization();
+				}
+			},
+			parent: this.filter_wrapper,
+			render_input: true,
+		});
+
+		this.workstation_type.$wrapper.addClass('form-column col-sm-2');
+
+		this.workstation = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Link',
+				options: 'Workstation',
+				fieldname: 'workstation',
+				label: __('Machine'),
+				onchange: () => {
+					this.render_plant_visualization();
+				},
+				get_query: () => {
+					if (this.workstation_type.get_value()) {
+						return {
+							filters: {
+								'workstation_type': this.workstation_type.get_value() || ''
+							}
+						}
+					}
+				}
+			},
+			parent: this.filter_wrapper,
+			render_input: true,
+		});
+
+		this.workstation.$wrapper.addClass('form-column col-sm-2');
+
+		this.workstation_status = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Select',
+				options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup',
+				fieldname: 'workstation_status',
+				label: __('Status'),
+				onchange: () => {
+					this.render_plant_visualization();
+				},
+			},
+			parent: this.filter_wrapper,
+			render_input: true,
+		});
+	}
+
+	render_plant_visualization() {
+		let plant_floor = this.plant_floor.get_value();
+
+		if (plant_floor) {
+			frappe.call({
+				method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
+				args: {
+					plant_floor: plant_floor,
+					workstation_type: this.workstation_type.get_value(),
+					workstation: this.workstation.get_value(),
+					workstation_status: this.workstation_status.get_value()
+				},
+				callback: (r) => {
+					this.workstations = r.message;
+					this.render_workstations();
+				}
+			});
+		}
+	}
+
+	render_workstations() {
+		this.wrapper.find('.plant-floor-container').empty();
+		let template  = frappe.render_template("visual_plant_floor_template", {
+			workstations: this.workstations
+		});
+
+		$(template).appendTo(this.wrapper.find('.plant-floor-container'));
+	}
+
+	prepare_menu() {
+		this.page.add_menu_item(__('Refresh'), () => {
+			this.render_plant_visualization();
+		});
+	}
+}
+
+frappe.ui.VisualPlantFloor = VisualPlantFloor;
\ No newline at end of file
diff --git a/erpnext/public/js/templates/visual_plant_floor_template.html b/erpnext/public/js/templates/visual_plant_floor_template.html
new file mode 100644
index 0000000..2e67085
--- /dev/null
+++ b/erpnext/public/js/templates/visual_plant_floor_template.html
@@ -0,0 +1,19 @@
+{% $.each(workstations, (idx, row) => { %}
+	<div class="workstation-wrapper">
+		<div class="workstation-image">
+			<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
+				<a class="workstation-image-link" href="{{row.workstation_link}}">
+					{% if(row.status_image) { %}
+						<img class="workstation-image-cls" src="{{row.status_image}}">
+					{% } else { %}
+						<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row.name, 2)}}</div>
+					{% } %}
+				</a>
+			</div>
+		</div>
+		<div class="workstation-card text-center">
+			<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
+			<div>{{row.workstation_name}}</div>
+		</div>
+	</div>
+{% }); %}
\ No newline at end of file
diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss
index 8ab5973..1626b7c 100644
--- a/erpnext/public/scss/erpnext.scss
+++ b/erpnext/public/scss/erpnext.scss
@@ -490,3 +490,53 @@
 .exercise-col {
 	padding: 10px;
 }
+
+.plant-floor, .workstation-wrapper, .workstation-card p {
+	border-radius: var(--border-radius-md);
+	border: 1px solid var(--border-color);
+	box-shadow: none;
+	background-color: var(--card-bg);
+	position: relative;
+}
+
+.plant-floor {
+	padding-bottom: 25px;
+}
+
+.plant-floor-filter {
+	padding-top: 10px;
+	display: flex;
+	flex-wrap: wrap;
+}
+
+.plant-floor-container {
+	display: grid;
+	grid-template-columns: repeat(6,minmax(0,1fr));
+	gap: var(--margin-xl);
+}
+
+@media screen and (max-width: 620px) {
+	.plant-floor-container {
+		grid-template-columns: repeat(2,minmax(0,1fr));
+	}
+}
+
+.plant-floor-container .workstation-card {
+	padding: 5px;
+}
+
+.plant-floor-container .workstation-image-link {
+	width: 100%;
+	font-size: 50px;
+	margin: var(--margin-sm);
+	min-height: 9rem;
+}
+
+.workstation-abbr {
+	display: flex;
+	background-color: var(--control-bg);
+	height:100%;
+	width:100%;
+	align-items: center;
+	justify-content: center;
+}
\ No newline at end of file