// ==UserScript==
// @name eRepublik Company Holdings Overview & Selector (2025-05-12, v3.4.5)
// @namespace http://tampermonkey.net/
// @version 3.4.5
// @description Provides an overview and partial company work-selection interface for eRepublik. This script operates solely on local browser data and does not save, store, or transmit any user data. Panels are draggable, collapsible, and include Reset, Info, and Donations buttons for transparency and support.
// @author Janko Fran
// @license Custom License - Personal, Non-Commercial Use Only
// @match https://www.erepublik.com/en/economy/myCompanies
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjNjY2Ii8+PHRleHQgeD0iMTIiIHk9IjE1IiBmb250LXNpemU9IjEwIiBmaWxsPSIjZmZmIj5Db21wYW55PC90ZXh0Pjwvc3ZnPg==
// @grant none
// @downloadURL none
// ==/UserScript==
/* License: This script is provided free of charge for personal, non-commercial use.
// You are granted a perpetual, royalty-free license to use this script on your own eRepublik account.
// No part of this script may be modified, redistributed, or used for commercial purposes without the express written permission of the author, Janko Fran.
// Donations are welcome to support future improvements. For details, see the Info modal or documentation.
//
// Donation Links:
// • eRepublik Donations: https://www.erepublik.com/en/economy/donate-money/2103304
// • Satoshi Donations: janko7ea63e4e@zbd.gg
// For custom scripts or financial donations, please contact:
// https://www.erepublik.com/en/main/messages-compose/2103304
*/
(function () {
'use strict';
/*************************
* Configuration Section *
*************************/
const config = {
defaultPositions: {
leftPanel: { top: "10px", left: "10px" },
workPanel: { top: "80px", left: "calc(100% - 370px)" }
},
panelTitles: {
leftPanel: "Company Holding(s) Overview Panel",
workPanel: "Work Selection Panel"
},
panelIcon: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjNjY2Ii8+PHRleHQgeD0iMTIiIHk9IjE1IiBmb250LXNpemU9IjEwIiBmaWxsPSIjZmZmIj5Db21wYW55PC90ZXh0Pjwvc3ZnPg==`,
leftPanelAlpha: 0.85,
overlayAlpha: 0.70,
workPanelPadding: "4px",
borderRadius: "8px",
buttonBorderRadius: "2px",
baseFontSize: "12px",
modalZIndex: 11000,
modalMaxWidth: "500px",
textLabels: {
overallHeader: "eRepublik Holdings Overview",
managerDetails: "Manager Mode Details",
employeeDetails: "Employee Mode Details",
holdingsOverview: "Aggregated Overview by Holding",
detailedOverview: "Detailed Overview by Holding",
totalCompanies: "Total Companies",
totalManager: "Total Manager",
totalEmployee: "Total Employee",
worked: "Worked",
assigned: "Assigned",
infoText: `
Personal Motivation
Since official development of eRepublik has slowed significantly in recent years, I decided to improve the player experience myself. This project began as a personal tool, and I’m sharing it for the benefit of other active players who still enjoy the game. In many ways, this is how the company workflow should have worked from the beginning. This project is a small contribution toward keeping eRepublik fun, efficient, and rewarding.
What the Script Does
This script helps you manage your eRepublik companies more efficiently by grouping them by industry type, quality, and holding, while giving you precise control over your work. It runs entirely in your browser session, with no data stored, transmitted, or shared externally.
Who Will Benefit
For tycoons managing hundreds or thousands of companies, this tool is indispensable. It lets you specify exactly how many companies to work in for each group, optimizing time, energy, and storage, all while avoiding repetitive clicking.
While the default Select all or Select none buttons may suffice for smaller company sets, this tool becomes especially useful when time is short or precision matters. Whether you're optimizing for speed, strategy, or limited energy, this tool offers a faster, more flexible alternative that gives you greater control over your workflow and adapts to your playstyle from casual to large-scale.
⚠️ Important Note
This script does not automate any actions beyond selecting companies in your browser. You still need to manually travel and click Work as Manager or perform other actions yourself. Its purpose is to enhance visibility and reduce manual clicking, without violating the game rules.
Free, Transparent, Player-Driven
This script is free, transparent, and built entirely with players in mind. There are no trackers, no ads, and no hidden behavior. It was created with genuine passion for the game and a commitment to fair, efficient, and enjoyable gameplay.
Tech Stack
This script was developed using the following technologies:
Tampermonkey: A userscript manager used to inject and run the script within your browser session.JavaScript (ES5): The script is written in vanilla JavaScript (ECMAScript 5) to ensure compatibility with older browsers and eRepublik’s frontend.HTML & CSS: Custom interface panels, modals, and styling are built using pure HTML and CSS, directly injected into the DOM.ChatGPT Plus: Used extensively to assist in development, testing, and refining the script across over 60 iterations during the period of 2 months.
License
For personal, non-commercial use only. Redistribution or commercial use is not permitted without the author's written consent.
Support Future Development
If this script has saved you time or made company management easier, please consider supporting future improvements of this and other scripts. Donations help cover development time, testing, and enhancements, and are a much-appreciated motivation to keep going.
`;
}
buildHoldingsOverview(aggregated, results) {
let holdingsHTML = `
${config.textLabels.holdingsOverview}
`;
for (let holdingName in results) {
let totalCount = 0;
const holdingData = results[holdingName];
for (let fType in holdingData.Factories) {
for (let qual in holdingData.Factories[fType]) totalCount += holdingData.Factories[fType][qual].count;
}
for (let subCat in holdingData["Raw Materials"]) {
for (let rType in holdingData["Raw Materials"][subCat]) totalCount += holdingData["Raw Materials"][subCat][rType].count;
}
totalCount += holdingData.Unknown;
if (totalCount === 0) continue;
holdingsHTML += `
${holdingName}
`;
let holdingManager = { completed: 0, total: 0, count: 0 };
for (let industry in aggregated[holdingName].manager) {
const group = aggregated[holdingName].manager[industry];
holdingManager.completed += group.completed;
holdingManager.total += group.total;
holdingManager.count += group.count;
}
if (holdingManager.count > 0) {
const holdingManagerPercentage = holdingManager.total > 0 ? ((holdingManager.completed / holdingManager.total) * 100).toFixed(1) : '0';
holdingsHTML += `
🧑💼 ${config.textLabels.totalManager}: ${config.textLabels.worked}: ${holdingManager.completed}/${holdingManager.total} (${holdingManagerPercentage}%) over ${holdingManager.count} companies
`;
for (let industry in aggregated[holdingName].manager) {
const group = aggregated[holdingName].manager[industry];
if (group.count === 0) continue;
const percentage = group.total > 0 ? ((group.completed / group.total) * 100).toFixed(1) : '0';
holdingsHTML += `
${industry}: ${config.textLabels.worked}: ${group.completed}/${group.total} (${percentage}%) over ${group.count} companies
`;
}
holdingsHTML += `
`;
}
let holdingEmployee = { completed: 0, total: 0, count: 0 };
for (let industry in aggregated[holdingName].employee) {
const group = aggregated[holdingName].employee[industry];
holdingEmployee.completed += group.completed;
holdingEmployee.total += group.total;
holdingEmployee.count += group.count;
}
if (holdingEmployee.count > 0) {
const holdingEmployeePercentage = holdingEmployee.total > 0 ? ((holdingEmployee.completed / holdingEmployee.total) * 100).toFixed(1) : '0';
holdingsHTML += `
👷 ${config.textLabels.totalEmployee}: ${config.textLabels.assigned}: ${holdingEmployee.completed}/${holdingEmployee.total} (${holdingEmployeePercentage}%) over ${holdingEmployee.count} companies
`;
for (let industry in aggregated[holdingName].employee) {
const group = aggregated[holdingName].employee[industry];
if (group.count === 0) continue;
const percentage = group.total > 0 ? ((group.completed / group.total) * 100).toFixed(1) : '0';
holdingsHTML += `
${industry}: ${config.textLabels.assigned}: ${group.completed}/${group.total} (${percentage}%) over ${group.count} companies
`;
}
holdingsHTML += `
`;
}
holdingsHTML += `
`;
}
holdingsHTML += `
`;
return holdingsHTML;
}
buildDetailedOverview(results) {
let detailedHTML = `
${config.textLabels.detailedOverview}
`;
for (let holdingName in results) {
let totalCount = 0;
const holdingData = results[holdingName];
for (let fType in holdingData.Factories) {
for (let qual in holdingData.Factories[fType]) totalCount += holdingData.Factories[fType][qual].count;
}
for (let subCat in holdingData["Raw Materials"]) {
for (let rType in holdingData["Raw Materials"][subCat]) totalCount += holdingData["Raw Materials"][subCat][rType].count;
}
totalCount += holdingData.Unknown;
if (totalCount === 0) continue;
detailedHTML += `
${holdingName}
`;
if (Object.keys(holdingData.Factories).length > 0) {
detailedHTML += ' Factories: ';
for (let fType in holdingData.Factories) {
detailedHTML += ` ${fType}: `;
let qualities = Object.keys(holdingData.Factories[fType]).sort((a, b) => parseInt(b.replace("Q", "")) - parseInt(a.replace("Q", "")));
qualities.forEach(qual => {
let group = holdingData.Factories[fType][qual];
let labelCompleted = group.workMode === "employee" ? config.textLabels.assigned : config.textLabels.worked;
let labelRemaining = group.workMode === "employee" ? "Assign" : "Not worked";
let line = `${qual}: Total Companies: ${group.count}, ${labelCompleted}: ${group.completed}/${group.totalSlots}`;
if (group.remaining > 0) line += ` [${labelRemaining}: ${group.remaining}]`;
detailedHTML += ` ${line} `;
});
}
}
if (Object.keys(holdingData["Raw Materials"]).length > 0) {
detailedHTML += ' Raw Materials: ';
for (let subCat in holdingData["Raw Materials"]) {
detailedHTML += ` ${subCat}: `;
for (let rType in holdingData["Raw Materials"][subCat]) {
if (!rType || rType === "undefined") continue;
let group = holdingData["Raw Materials"][subCat][rType];
let labelCompleted = group.workMode === "employee" ? config.textLabels.assigned : config.textLabels.worked;
let labelRemaining = group.workMode === "employee" ? "Assign" : "Not worked";
let line = `${rType}: Total: ${group.count}, ${labelCompleted}: ${group.completed}/${group.totalSlots}`;
if (group.remaining > 0) line += ` [${labelRemaining}: ${group.remaining}]`;
detailedHTML += ` ${line} `;
}
}
}
if (holdingData.Unknown > 0) detailedHTML += ` Unknown: ${holdingData.Unknown} `;
detailedHTML += `
`;
}
detailedHTML += `
`;
return detailedHTML;
}
renderFactoryGroup(group) {
let out = '';
for (let qual in group.qualityBreakdown) {
const sampleCompany = group.companies.find(c => c.querySelector(".mini_stars")?.className.includes(`q${qual.replace("Q", "")}`));
const bonus = sampleCompany?.querySelector(".area_final_products .resource_bonus")?.textContent.trim() || "-";
out += `- ${qual}: ${group.qualityBreakdown[qual]} | Bonus: ${bonus} `;
}
return out;
}
renderRawMaterialGroup(group) {
let out = '';
for (let t in group.typeBreakdown || {}) {
// Use the DataProcessor's extractCompanyData() method here
const sampleCompany = group.companies.find(c =>
this.dataProcessor.extractCompanyData(c).type === t
);
let bonus = "-";
if (sampleCompany) {
const bonusEl =
// First try the original working selector:
sampleCompany.querySelector(".area_final_products .resource_bonus") ||
// Then try other fallbacks:
sampleCompany.querySelector(".resource_bonus") ||
sampleCompany.querySelector(".area_bonus .resource_bonus") ||
sampleCompany.querySelector(".bonus_icon + span") ||
sampleCompany.querySelector(".bonus_area .resource_bonus") ||
// Last chance: scan all spans/divs
[...sampleCompany.querySelectorAll("span, div")]
.find(el => el.textContent.trim().match(/^\+\d+%$/));
bonus = bonusEl?.textContent.trim() || "-";
}
out += `- ${t}: ${group.typeBreakdown[t]} | Bonus: ${bonus} `;
}
return out;
}
}
/**
* Utils
* -----
* Utility class providing stateless helper functions.
* - sanitize(str): Converts strings into ID-safe format.
*/
class Utils {
static sanitize(str) {
return str.replace(/\W/g, '_');
}
}
/**
* Initialization
* --------------
* Orchestrates initial DOM scan, debounce observation,
* auto-expansion of collapsed holdings, and rendering of panels.
*
* Logic:
* - Injects all styles and modals.
* - Checks if company groups are present; if yes, proceeds.
* - Otherwise, uses MutationObserver + debounce to detect late-load.
* - Includes 5-second fallback timeout.
*/
const domManager = new DOMManager();
const dataProcessor = new DataProcessor(domManager);
const workSelector = new WorkSelector(domManager, dataProcessor);
const uiManager = new UIManager(dataProcessor);
function expandAllHoldings() {
document.querySelectorAll('.companies_group').forEach(group => {
const header = group.querySelector('.companies_header');
const listing = group.querySelector('.companies_listing');
if (header && listing && listing.style.display === 'none') {
header.click(); // Simulate click to expand
}
});
}
function renderInterface() {
const holdingsData = dataProcessor.groupCompaniesByHolding();
uiManager.renderOverviewPanel(holdingsData, dataProcessor);
const selectionData = workSelector.buildSelectionData(holdingsData);
uiManager.renderWorkSelectionPanel(selectionData, (holding, groupName, limit, showPopup) => {
workSelector.selectCompanies(selectionData[holding][groupName], holding, groupName, limit, showPopup);
}, dataProcessor, workSelector);
}
function initialize() {
StyleManager.injectAll();
uiManager.createInfoModal();
if (domManager.getHoldingGroups().length > 0) {
console.log('[DEBUG] Companies group already present, initializing...');
expandAllHoldings();
renderInterface();
} else {
let debounceTimeout = null;
const observer = new MutationObserver((mutations, obs) => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
if (domManager.getHoldingGroups().length > 0) {
console.log('[DEBUG] Companies group detected (debounced), initializing...');
obs.disconnect();
expandAllHoldings();
renderInterface();
}
}, 200); // 200ms debounce
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
if (domManager.getHoldingGroups().length > 0) {
console.log('[DEBUG] Fallback initialization after 5s');
renderInterface();
} else {
console.log('[DEBUG] No company groups found after 5s, GUI not initialized.');
}
}, 5000);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize);
} else {
initialize();
}
})();