diff --git a/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.js b/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.js index 4d115662f..5cbc67a16 100644 --- a/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.js +++ b/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.js @@ -189,4 +189,25 @@ function fetch_advances_from_mcts(frm) { }, }); } -} \ No newline at end of file +} + +/** + * Fetch cost head from the item doctype. + */ +function set_cost_head(cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.item_code) { + frappe.db.get_value('Item', row.item_code, 'cost_head') + .then(r => { + if (r.message && r.message.cost_head) { + frappe.model.set_value(cdt, cdn, 'cost_head', r.message.cost_head); + } + }); + } +} + +frappe.ui.form.on('Purchase Invoice Item', { + item_code: function(frm, cdt, cdn) { + set_cost_head(cdt, cdn); + } +}); diff --git a/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.py b/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.py index 0f3bd1195..6275d2c71 100644 --- a/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.py +++ b/beams/beams/custom_scripts/purchase_invoice/purchase_invoice.py @@ -33,3 +33,13 @@ def set_from_bureau_flag(doc, method): if "Bureau User" in frappe.get_roles(user): doc.from_bureau = 1 +def set_cost_head_from_item(doc, method=None): + """ + Fetch cost head from item doctype + """ + for row in doc.get("items"): + if not row.item_code: + continue + item_cost_head = frappe.db.get_value("Item", row.item_code, "cost_head") + if item_cost_head: + row.cost_head = item_cost_head diff --git a/beams/beams/custom_scripts/purchase_order/purchase_order.js b/beams/beams/custom_scripts/purchase_order/purchase_order.js index 46e5f6f4b..97ba34c4e 100644 --- a/beams/beams/custom_scripts/purchase_order/purchase_order.js +++ b/beams/beams/custom_scripts/purchase_order/purchase_order.js @@ -48,3 +48,24 @@ function clear_checkbox_exceed(frm){ frm.set_value("is_budget_exceeded", 0); } } + +/** + * Fetch cost head from the item doctype. + */ +function set_cost_head(cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.item_code) { + frappe.db.get_value('Item', row.item_code, 'cost_head') + .then(r => { + if (r.message && r.message.cost_head) { + frappe.model.set_value(cdt, cdn, 'cost_head', r.message.cost_head); + } + }); + } +} + +frappe.ui.form.on('Purchase Order Item', { + item_code: function(frm, cdt, cdn) { + set_cost_head(cdt, cdn); + } +}); diff --git a/beams/beams/custom_scripts/sales_invoice/sales_invoice.js b/beams/beams/custom_scripts/sales_invoice/sales_invoice.js index 6ac985647..2209a5c88 100644 --- a/beams/beams/custom_scripts/sales_invoice/sales_invoice.js +++ b/beams/beams/custom_scripts/sales_invoice/sales_invoice.js @@ -74,3 +74,25 @@ function check_include_in_ibf(frm) { }); } } + +/** + * Fetch cost head from the item doctype. + */ +function set_cost_head(cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.item_code) { + frappe.db.get_value('Item', row.item_code, 'cost_head') + .then(r => { + if (r.message && r.message.cost_head) { + frappe.model.set_value(cdt, cdn, 'cost_head', r.message.cost_head); + } + }); + } +} + +frappe.ui.form.on('Sales Invoice Item', { + item_code: function(frm, cdt, cdn) { + set_cost_head(cdt, cdn); + } +}); + diff --git a/beams/beams/custom_scripts/sales_order/sales_order.js b/beams/beams/custom_scripts/sales_order/sales_order.js index 3e07265a2..bcbf77854 100644 --- a/beams/beams/custom_scripts/sales_order/sales_order.js +++ b/beams/beams/custom_scripts/sales_order/sales_order.js @@ -137,3 +137,24 @@ function check_is_agent_from_customer(frm) { }); } } + +/** + * Fetch cost head from the item doctype. + */ +function set_cost_head(cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.item_code) { + frappe.db.get_value('Item', row.item_code, 'cost_head') + .then(r => { + if (r.message && r.message.cost_head) { + frappe.model.set_value(cdt, cdn, 'cost_head', r.message.cost_head); + } + }); + } +} + +frappe.ui.form.on('Sales Order Item', { + item_code: function(frm, cdt, cdn) { + set_cost_head(cdt, cdn); + } +}); diff --git a/beams/beams/doctype/batta_claim/batta_claim.json b/beams/beams/doctype/batta_claim/batta_claim.json index 3b494027c..060f8aa0d 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.json +++ b/beams/beams/doctype/batta_claim/batta_claim.json @@ -287,11 +287,10 @@ "label": "Is Delhi Bureau" }, { - "depends_on": "eval:doc.workflow_state==\"Pending Approval\"", + "depends_on": "eval:(frappe.user.has_role(\"HOD\") || frappe.user.has_role(\"Admin User\"))", "fieldname": "expense_type", "fieldtype": "Select", "label": "Expense Type", - "mandatory_depends_on": "eval:doc.workflow_state==\"Pending Approval\"", "options": "\nDirect\nIndirect", "permlevel": 1 }, @@ -346,7 +345,7 @@ "link_fieldname": "batta_claim_reference" } ], - "modified": "2026-02-05 14:16:38.084292", + "modified": "2026-04-06 12:13:50.993598", "modified_by": "Administrator", "module": "BEAMS", "name": "Batta Claim", diff --git a/beams/beams/doctype/batta_claim/batta_claim.py b/beams/beams/doctype/batta_claim/batta_claim.py index 7ee0174a2..3b718c430 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.py +++ b/beams/beams/doctype/batta_claim/batta_claim.py @@ -24,7 +24,7 @@ def set_from_bureau_flag(self): self.from_bureau = 1 def on_submit(self): - if self.workflow_state == 'Approved': + if self.workflow_state in ["Approved by CEO", "Approved"] and self.docstatus == 1: if not self.expense_type: frappe.throw( title="Expense Type Required", diff --git a/beams/beams/doctype/beams_accounts_settings/beams_accounts_settings.json b/beams/beams/doctype/beams_accounts_settings/beams_accounts_settings.json index 8c26946f8..9a6f58b86 100644 --- a/beams/beams/doctype/beams_accounts_settings/beams_accounts_settings.json +++ b/beams/beams/doctype/beams_accounts_settings/beams_accounts_settings.json @@ -34,7 +34,8 @@ "column_break_jkfg", "rent_expense_item", "batta_ot_expense_item", - "advance_account" + "advance_account", + "batta_expense_cost_head" ], "fields": [ { @@ -203,12 +204,18 @@ "fieldtype": "Link", "label": "Advance Account", "options": "Account" + }, + { + "fieldname": "batta_expense_cost_head", + "fieldtype": "Link", + "label": "Batta Expense Cost Head", + "options": "Cost Head" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-17 14:51:14.096058", + "modified": "2026-03-31 19:44:46.985532", "modified_by": "Administrator", "module": "BEAMS", "name": "Beams Accounts Settings", diff --git a/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.json b/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.json index ff9a812ec..fba8d0a12 100644 --- a/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.json +++ b/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.json @@ -23,6 +23,7 @@ "ending_date_and_time", "check_in_time", "total_hours", + "last_trip", "section_break_tjbr", "purpose", "fuel_details_section", @@ -341,12 +342,18 @@ "fieldname": "check_in_time", "fieldtype": "Datetime", "label": "Check In Time" + }, + { + "default": "0", + "fieldname": "last_trip", + "fieldtype": "Check", + "label": "Last Trip" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-26 15:50:44.470313", + "modified": "2026-04-01 10:46:15.616295", "modified_by": "Administrator", "module": "BEAMS", "name": "Bureau Trip Sheet", diff --git a/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.py b/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.py index a0c4505fb..5112db2dc 100644 --- a/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.py +++ b/beams/beams/doctype/bureau_trip_sheet/bureau_trip_sheet.py @@ -53,6 +53,20 @@ def calculate_batta(self): ''' Calculate total trip batta from daily rate × number of days (or food allowance total). ''' + policy = get_batta_policy_values(supplier=self.supplier) + + if policy.get("is_actual_with") or policy.get("is_actual_without") or policy.get("is_actual_food"): + + self.total_food_allowance = ( + flt(self.breakfast) + + flt(self.lunch) + + flt(self.dinner) + ) + + self.batta = self.total_food_allowance + + return + if self.total_food_allowance: self.batta = self.total_food_allowance else: @@ -105,14 +119,25 @@ def calculate_total_daily_batta(self): self.total_daily_batta = flt(self.batta) or 0 def calculate_total_ot_batta(self): - """Total OT batta = (total_hours - ot_working_hours) * ot_batta rate, when supplier and hours are set.""" + """ + Calculate OT batta ONLY when last_trip is checked + """ + + if not self.last_trip: + self.total_ot_batta = 0 + return + total_hours = flt(self.total_hours or 0) ot_rate = flt(self.ot_batta or 0) + if not self.supplier or not total_hours: self.total_ot_batta = 0 return + ot_working_hours = flt(get_ot_working_hours(self.supplier) or 0) + ot_hours = max(0, total_hours - ot_working_hours) + self.total_ot_batta = round(ot_hours * ot_rate, 2) def calculate_daily_batta(self): @@ -126,6 +151,11 @@ def calculate_daily_batta(self): - Else → No Allowance When policy allows actual (editable) daily amounts, user-entered values are preserved on save. ''' + policy = get_batta_policy_values(supplier=self.supplier) + + if policy.get("is_actual_with") or policy.get("is_actual_without") or policy.get("is_actual_food"): + return + # Preserve manually entered daily rates before reset. manual_daily_batta_without_overnight = flt(self.get("daily_batta_without_overnight_stay")) manual_daily_batta_with_overnight = flt(self.get("daily_batta_with_overnight_stay")) diff --git a/beams/beams/doctype/monthly_consolidated_trip_sheet/monthly_consolidated_trip_sheet.py b/beams/beams/doctype/monthly_consolidated_trip_sheet/monthly_consolidated_trip_sheet.py index fb6da9575..5d74f968c 100644 --- a/beams/beams/doctype/monthly_consolidated_trip_sheet/monthly_consolidated_trip_sheet.py +++ b/beams/beams/doctype/monthly_consolidated_trip_sheet/monthly_consolidated_trip_sheet.py @@ -301,6 +301,7 @@ def create_journal_entry(monthly_consolidated_trip_sheet_name): fuel_expense_account = settings.get("fuel_expense_item") ot_account = settings.get("batta_ot_expense_item") fuel_card_account = settings.get("fuel_card_account") + batta_cost_head = settings.get("batta_expense_cost_head") supplier_payable_account = _get_supplier_payable_account(doc.supplier, company) if not supplier_payable_account: @@ -336,18 +337,21 @@ def create_journal_entry(monthly_consolidated_trip_sheet_name): "account": batta_account, "debit_in_account_currency": total_batta_je, "credit_in_account_currency": 0, + "cost_head": batta_cost_head }) if ot_account and total_ot_je: accounts.append({ "account": ot_account, "debit_in_account_currency": total_ot_je, "credit_in_account_currency": 0, + "cost_head": batta_cost_head }) if fuel_expense_account and total_fuel_expense: accounts.append({ "account": fuel_expense_account, "debit_in_account_currency": total_fuel_expense, "credit_in_account_currency": 0, + "cost_head": batta_cost_head }) # Credits — fuel card / fuel log only (advance is already reflected in batta_after / ot_after) if fuel_card_account and total_fuel_log: @@ -355,6 +359,7 @@ def create_journal_entry(monthly_consolidated_trip_sheet_name): "account": fuel_card_account, "debit_in_account_currency": 0, "credit_in_account_currency": total_fuel_log, + "cost_head": batta_cost_head }) # Supplier balancing: credit when company owes supplier, debit when recovering if supplier_payable_account and supplier_amount != 0: @@ -364,6 +369,7 @@ def create_journal_entry(monthly_consolidated_trip_sheet_name): "party": doc.supplier, "debit_in_account_currency": abs(supplier_amount) if supplier_amount < 0 else 0, "credit_in_account_currency": supplier_amount if supplier_amount > 0 else 0, + "cost_head": batta_cost_head }) if not accounts: diff --git a/beams/hooks.py b/beams/hooks.py index b56afcb8b..e071a3d30 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -197,7 +197,10 @@ "Sales Invoice": { "on_update_after_submit":"beams.beams.custom_scripts.sales_invoice.sales_invoice.on_update_after_submit", "autoname": "beams.beams.custom_scripts.sales_invoice.sales_invoice.autoname", - "validate": "beams.beams.custom_scripts.sales_invoice.sales_invoice.validate_sales_invoice_for_barter" + "validate": [ + "beams.beams.custom_scripts.sales_invoice.sales_invoice.validate_sales_invoice_for_barter", + "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.set_cost_head_from_item", + ], }, "Quotation": { "validate": "beams.beams.custom_scripts.quotation.quotation.validate_is_barter", @@ -208,6 +211,7 @@ "before_save": "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.before_save", "before_insert": "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.set_from_bureau_flag", "before_validate": "beams.beams.custom_scripts.purchase_order.purchase_order.set_is_budgeted", + "validate": "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.set_cost_head_from_item", }, "Account": { "after_insert": "beams.beams.custom_scripts.account.account.create_todo_on_creation_for_account" @@ -229,7 +233,10 @@ }, "Purchase Order": { "on_update": "beams.beams.custom_scripts.purchase_order.purchase_order.create_todo_on_finance_verification", - "validate": "beams.beams.custom_scripts.purchase_order.purchase_order.validate_reason_for_rejection", + "validate": [ + "beams.beams.custom_scripts.purchase_order.purchase_order.validate_reason_for_rejection", + "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.set_cost_head_from_item", + ], "before_validate": "beams.beams.custom_scripts.purchase_order.purchase_order.set_is_budgeted", "on_change":"beams.beams.custom_scripts.purchase_order.purchase_order.update_equipment_quantities", }, @@ -243,7 +250,8 @@ "Sales Order": { "autoname": "beams.beams.custom_scripts.sales_order.sales_order.autoname", "before_save": "beams.beams.custom_scripts.sales_order.sales_order.validate_sales_order_amount_with_quotation", - "before_insert": "beams.beams.custom_scripts.sales_order.sales_order.set_region_from_quotation" + "before_insert": "beams.beams.custom_scripts.sales_order.sales_order.set_region_from_quotation", + "validate": "beams.beams.custom_scripts.purchase_invoice.purchase_invoice.set_cost_head_from_item", }, "Contract": { "on_update": "beams.beams.custom_scripts.contract.contract.create_todo_on_contract_verified_by_finance", diff --git a/beams/setup.py b/beams/setup.py index b51e8e843..d9d2d8311 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -1999,6 +1999,13 @@ def get_item_custom_fields(): "fieldtype": "Check", "label": "Is Bundle Item", "insert_after": "has_variants" + }, + { + "fieldname": "cost_head", + "fieldtype": "Link", + "label": "Cost Head", + "options": "Cost Head", + "insert_after": "item_defaults" } ] } diff --git a/beams/www/roster/index.css b/beams/www/roster/index.css new file mode 100644 index 000000000..53041cc68 --- /dev/null +++ b/beams/www/roster/index.css @@ -0,0 +1,638 @@ +/* ─── Frappe Design Tokens ──────────────────────────────────────── */ +:root { + --primary: #2490ef; + --primary-light: rgba(36, 144, 239, 0.1); + --primary-dark: #1c72be; + --text-color: #1c2126; + --text-muted: #8d99a6; + --text-light: #c4c4c4; + --border-color: #d1d8dd; + --subtle-border: #ebeff2; + --bg-color: #f0f4f7; + --card-bg: #ffffff; + --input-bg: #ffffff; + --navbar-bg: #ffffff; + --red: #e24c4b; + --yellow: #f8a100; + --green: #29cd42; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .06), 0 1px 3px rgba(0, 0, 0, .10); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .10), 0 2px 4px -1px rgba(0, 0, 0, .06); + --border-radius: 6px; + --border-radius-md: 8px; + --border-radius-lg: 10px; + --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --page-head-height: 56px; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font); + font-size: 13px; + color: var(--text-color); + background: var(--bg-color); + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +/* ─── Page Wrapper ───────────────────────────────────────────────── */ +.page-wrapper { + min-height: calc(100vh - 48px); +} + +/* ─── Page Head ──────────────────────────────────────────────────── */ +.page-head { + background: var(--card-bg); + border-bottom: 1px solid var(--border-color); + padding: 0 24px; + height: var(--page-head-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + position: sticky; + top: 48px; + z-index: 100; +} + +.page-head-left { + display: flex; + flex-direction: column; + gap: 2px; +} + +.page-title { + font-size: 16px; + font-weight: 600; + color: var(--text-color); + line-height: 1; +} + +.page-head-right { + display: flex; + align-items: center; + gap: 8px; +} + +/* ─── Frappe Buttons ─────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + font-family: var(--font); + font-size: 12px; + font-weight: 500; + border-radius: var(--border-radius); + cursor: pointer; + transition: background 0.12s, border-color 0.12s, box-shadow 0.12s; + white-space: nowrap; + text-decoration: none; + border: 1px solid transparent; + line-height: 1; +} + +.btn-sm { + padding: 5px 10px; + font-size: 11px; +} + +.btn-md { + padding: 7px 14px; +} + +.btn-default { + background: var(--card-bg); + border-color: var(--border-color); + color: var(--text-color); +} + +.btn-default:hover { + background: var(--bg-color); + border-color: #b8c2cc; +} + +.btn-default:active { + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} + +.btn-primary { + background: var(--primary); + border-color: var(--primary); + color: #fff; +} + +.btn-primary:hover { + background: var(--primary-dark); + border-color: var(--primary-dark); +} + +.btn-danger { + background: var(--card-bg); + border-color: var(--border-color); + color: var(--red); +} + +.btn-danger:hover { + background: #fef2f2; + border-color: #fca5a5; +} + +/* ─── View Toggle (Frappe-style segmented) ───────────────────────── */ +.view-toggle { + display: flex; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 2px; + gap: 2px; +} + +.view-toggle .btn { + padding: 4px 12px; + font-size: 12px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 4px; +} + +.view-toggle .btn.active { + background: var(--card-bg); + color: var(--text-color); + box-shadow: var(--shadow-sm); +} + +/* ─── Page Body ──────────────────────────────────────────────────── */ +.page-body { + padding: 20px 24px; +} + +/* ─── Frappe Card ────────────────────────────────────────────────── */ +.frappe-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +/* ─── Roster Toolbar ─────────────────────────────────────────────── */ +.roster-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--subtle-border); +} + +.toolbar-left { + display: flex; + align-items: center; + gap: 8px; +} + +.date-range-label { + font-size: 13px; + font-weight: 500; + color: var(--text-color); +} + +/* ─── Roster Table Wrapper ───────────────────────────────────────── */ +.roster-table-wrap { + overflow-x: auto; + overflow-y: hidden; +} + +/* ─── Roster Table ───────────────────────────────────────────────── */ +.roster-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + table-layout: fixed; +} + +.roster-table th, +.roster-table td { + border-right: 1px solid var(--subtle-border); +} + +.roster-table th:last-child, +.roster-table td:last-child { + border-right: none; +} + +/* thead */ +.roster-table thead tr { + border-bottom: 1px solid var(--border-color); +} + +.th-shift { + padding: 8px 12px; + text-align: left; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + position: sticky; + left: 0; + background: var(--card-bg); + z-index: 10; +} + +.th-day { + padding: 6px 4px; + text-align: center; + font-weight: 400; + font-size: 11px; + background: var(--card-bg); +} + +.th-day.today { + background: var(--primary-light); +} + +.th-day .day-name { + color: var(--text-muted); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.th-day .day-num { + font-size: 14px; + font-weight: 600; + color: var(--text-color); + margin-top: 1px; +} + +.th-day.today .day-name, +.th-day.today .day-num { + color: var(--primary); +} + +.th-day.monthly .day-num { + font-size: 11px; +} + +/* tbody */ +.roster-table tbody tr { + border-bottom: 1px solid var(--subtle-border); + transition: background 0.1s; +} + +.roster-table tbody tr:last-child { + border-bottom: none; +} + +.roster-table tbody tr:hover>td { + background: #f8fafc; +} + +.roster-table tbody tr:hover>.td-shift { + background: #f8fafc; +} + +.td-shift { + padding: 10px 12px; + position: sticky; + left: 0; + background: var(--card-bg); + z-index: 5; + transition: background 0.1s; +} + +.shift-name { + font-weight: 600; + font-size: 12px; + color: var(--text-color); +} + +.shift-time { + font-size: 10px; + color: var(--text-muted); + margin-top: 1px; +} + +.td-cell { + padding: 5px 4px; + vertical-align: top; + background: transparent; +} + +/* Employee chip */ +.emp-chip { + border-radius: 4px; + padding: 2px 6px; + font-size: 10px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 2px; +} + +.emp-chip.monthly { + padding: 2px 3px; + text-align: center; +} + +.more-count { + font-size: 9px; + color: var(--text-muted); + text-align: center; +} + +/* Add button */ +.add-btn { + display: flex; + align-items: center; + justify-content: center; + margin: 1px auto 0; + border: 1px dashed var(--border-color); + background: transparent; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 14px; + font-weight: 400; + color: var(--text-muted); + transition: all 0.12s; + padding: 0; + line-height: 1; +} + +.add-btn:hover { + background: var(--primary-light); + border-color: var(--primary); + color: var(--primary); +} + +.add-btn.weekly { + width: 28px; + height: 22px; +} + +.add-btn.monthly { + width: 20px; + height: 16px; +} + +/* ─── Shift color legends ────────────────────────────────────────── */ +.legend-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + border-top: 1px solid var(--subtle-border); + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-muted); +} + +.legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +/* ─── Modal ──────────────────────────────────────────────────────── */ +.modal-backdrop { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, .45); + + /* Higher than Frappe page layers */ + z-index: 10000; + + align-items: center; + justify-content: center; + + /* allow clicks */ + pointer-events: auto; +} + +.modal-backdrop.open { + display: flex; +} + +.modal-dialog { + background: #fff; + width: 340px; + max-height: 540px; + overflow: auto; + + position: relative; + z-index: 10001; + + /* IMPORTANT */ + pointer-events: auto; +} + +.modal-dialog * { + pointer-events: auto !important; +} + +@keyframes modal-in { + from { + opacity: 0; + transform: translateY(-8px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 16px 16px 12px; + border-bottom: 1px solid var(--subtle-border); +} + +.modal-title { + font-size: 14px; + font-weight: 600; + color: var(--text-color); +} + +.modal-subtitle { + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} + +.modal-close { + border: none; + background: none; + font-size: 16px; + cursor: pointer; + color: var(--text-muted); + padding: 0; + line-height: 1; + margin-left: 8px; + flex-shrink: 0; +} + +.modal-close:hover { + color: var(--text-color); +} + +.modal-body { + padding: 12px 16px; + overflow-y: auto; + flex: 1; +} + +/* Frappe-style search input */ +.frappe-input { + width: 100%; + padding: 6px 10px; + font-family: var(--font); + font-size: 12px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--input-bg); + color: var(--text-color); + outline: none; + transition: border-color 0.12s, box-shadow 0.12s; +} + +.frappe-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 2px var(--primary-light); +} + +.frappe-input::placeholder { + color: var(--text-muted); +} + +/* Employee list */ +.emp-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 8px; + border-radius: var(--border-radius); + cursor: pointer; + transition: background 0.1s; +} + +.emp-item:hover { + background: var(--bg-color); +} + +.emp-name { + font-size: 12px; + color: var(--text-color); +} + +.no-results { + font-size: 12px; + color: var(--text-muted); + text-align: center; + padding: 12px 0; +} + +/* Avatar */ +.avatar { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; + flex-shrink: 0; + background: var(--primary-light); + color: var(--primary); +} + +.avatar.green { + background: rgba(41, 205, 66, 0.12); + color: #1a9c30; +} + +.avatar.sm { + width: 24px; + height: 24px; + font-size: 9px; +} + +/* Assigned section */ +.assigned-section { + display: none; + margin-top: 10px; + border-top: 1px solid var(--subtle-border); + padding-top: 10px; +} + +.section-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + margin-bottom: 6px; +} + +.assigned-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 8px; + border-radius: var(--border-radius); + margin-bottom: 3px; + background: var(--bg-color); +} + +.assigned-left { + display: flex; + align-items: center; + gap: 8px; +} + +.remove-btn { + border: none; + background: none; + font-size: 13px; + cursor: pointer; + color: var(--text-muted); + padding: 0 2px; + line-height: 1; + font-family: var(--font); + transition: color 0.1s; +} + +.remove-btn:hover { + color: var(--red); +} + +/* ─── Indicator dot ──────────────────────────────────────────────── */ +.indicator { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + margin-right: 4px; + vertical-align: middle; +} \ No newline at end of file diff --git a/beams/www/roster/index.html b/beams/www/roster/index.html new file mode 100644 index 000000000..3999ed238 --- /dev/null +++ b/beams/www/roster/index.html @@ -0,0 +1,100 @@ +{% extends "templates/base.html" %} +{% block content %} + + + + +
+ + +