diff --git a/arma/client/addons/org/ui/_site/base.css b/arma/client/addons/org/ui/_site/base.css new file mode 100644 index 0000000..a05b977 --- /dev/null +++ b/arma/client/addons/org/ui/_site/base.css @@ -0,0 +1,175 @@ +:root { + --bg-app: #fdfcf8; + --bg-surface: #ffffff; + --bg-surface-hover: #f1f5f9; + --primary: #475569; + --primary-hover: #1e293b; + --text-main: #1f2937; + --text-muted: #64748b; + --text-inverse: #f8fafc; + --border: #e2e8f0; + --radius: 8px; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --footer-bg: #1e293b; +} + +body { + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; + margin: 0; + padding: 0; + background: var(--bg-app); + color: var(--text-main); + line-height: 1.6; +} + +#app { + min-height: 100vh; +} + +main { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.container { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 2rem; + flex: 1; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.header { + text-align: center; + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border); + + h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + letter-spacing: -0.025em; + color: var(--primary-hover); + } + + p { + color: var(--text-muted); + font-size: 1.1rem; + } +} + +.card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; + box-shadow: var(--shadow); + text-align: center; + + h2 { + margin-top: 0; + font-size: 1.8rem; + color: var(--primary-hover); + } +} + +button { + background: var(--primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--radius); + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.65; + transform: none; + box-shadow: none; + } + + & + & { + margin-left: 1rem; + } +} + +.footer { + margin-top: auto; + background: var(--footer-bg); + color: var(--text-inverse); + display: block; + + .wrapper { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 3rem 2rem; + box-sizing: border-box; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + } + + h3 { + color: var(--text-inverse); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 700; + margin-bottom: 1.5rem; + border-bottom: 1px solid #475569; + padding-bottom: 0.5rem; + margin-right: 1rem; + } + + ul { + li { + color: #cbd5e1; + font-size: 0.95rem; + margin-bottom: 0.75rem; + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: white; + } + } + } +} + +@media (max-width: 960px) { + .container { + padding: 1.5rem; + } + + .header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + + h1 { + font-size: 2rem; + } + } + + .footer .wrapper { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/org/ui/_site/bootstrap.js b/arma/client/addons/org/ui/_site/bootstrap.js new file mode 100644 index 0000000..663a0b3 --- /dev/null +++ b/arma/client/addons/org/ui/_site/bootstrap.js @@ -0,0 +1,6 @@ +/** + * Registry app bootstrap + */ + +const root = document.getElementById("app"); +window.RegistryApp.runtime.render(window.RegistryApp.components.App, root); diff --git a/arma/client/addons/org/ui/_site/components/createOrgForm.js b/arma/client/addons/org/ui/_site/components/createOrgForm.js new file mode 100644 index 0000000..2541103 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/createOrgForm.js @@ -0,0 +1,168 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() { + const handleCreate = () => { + const data = { + orgName: String( + document.getElementById("org-create-name")?.value || "", + ), + type: String( + document.getElementById("org-create-type")?.value || "", + ), + }; + console.log("Org Registration:", data); + }; + + return h( + "div", + { className: "split-container" }, + h( + "div", + { className: "info-panel" }, + h("h2", null, "Registration Details"), + h( + "p", + null, + "Complete the form to add your organization to the Global Organization Registry.", + ), + h( + "ul", + { + style: { + textAlign: "left", + marginTop: "1.5rem", + listStyleType: "none", + padding: 0, + }, + }, + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Official Organization Designator", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Secure Comms Channel", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Deployment Roster Access", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ After-Action Report Tools", + ), + ), + h( + "div", + { + className: "price-tag", + style: { + marginTop: "2rem", + padding: "1rem", + background: "var(--bg-app)", + borderRadius: "var(--radius)", + border: "1px solid var(--border)", + }, + }, + h( + "span", + { + style: { + display: "block", + fontSize: "0.9rem", + color: "var(--text-muted)", + }, + }, + "Registration Fee", + ), + h( + "span", + { + style: { + display: "block", + fontSize: "2rem", + fontWeight: "700", + color: "var(--primary)", + }, + }, + "$50,000", + ), + ), + ), + h( + "div", + { className: "form-panel card", style: { margin: 0 } }, + h("h2", null, "Organization Registration"), + h( + "div", + { className: "app-form" }, + h( + "div", + null, + h("label", null, "Organization Name"), + h("input", { + id: "org-create-name", + type: "text", + placeholder: "e.g. Task Force 141", + }), + ), + h( + "div", + null, + h("label", null, "Organization Type"), + h( + "select", + { id: "org-create-type" }, + h( + "option", + { value: "infantry" }, + "Infantry / Milsim", + ), + h("option", { value: "aviation" }, "Aviation Wing"), + h( + "option", + { value: "pmc" }, + "Private Military Company", + ), + h( + "option", + { value: "support" }, + "Logistics & Support", + ), + ), + ), + h( + "div", + { className: "form-actions" }, + h( + "button", + { + type: "button", + style: { width: "100%" }, + onClick: handleCreate, + }, + "Submit Registration", + ), + h( + "span", + { + className: "cancel-link", + onClick: () => store.setView("home"), + }, + "Cancel / Return to Main", + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/footer.js b/arma/client/addons/org/ui/_site/components/footer.js new file mode 100644 index 0000000..76f53c4 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/footer.js @@ -0,0 +1,43 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.Footer = function Footer() { + return h( + "div", + { className: "footer" }, + h( + "div", + { className: "wrapper" }, + h( + "div", + null, + h("h3", null, "Registry Resources"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Registration Guidelines"), + h("li", null, "Tax & Fee Schedule"), + h("li", null, "Legal Compliance"), + h("li", null, "Trademark Database"), + ), + ), + h( + "div", + null, + h("h3", null, "Bureau Support"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Office: Sector 7 Admin Block"), + h("li", null, "Hours: 0800 - 1600 (GST)"), + h("li", null, "Helpdesk: 555-01-REGISTRY"), + h("li", null, "support@org-bureau.gov"), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/forms.css b/arma/client/addons/org/ui/_site/components/forms.css new file mode 100644 index 0000000..8364d77 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/forms.css @@ -0,0 +1,72 @@ +.split-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: center; + width: 100%; +} + +.info-panel { + text-align: left; + padding: 1rem; +} + +.app-form { + display: flex; + flex-direction: column; + gap: 1rem; + text-align: left; + + label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; + } + + input, + select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); + } + } +} + +.form-actions { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.cancel-link { + font-size: 0.9rem; + color: var(--text-muted); + cursor: pointer; + text-decoration: underline; + + &:hover { + color: var(--primary); + } +} + +@media (max-width: 960px) { + .split-container { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/org/ui/_site/components/header.js b/arma/client/addons/org/ui/_site/components/header.js new file mode 100644 index 0000000..bfb199c --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/header.js @@ -0,0 +1,23 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.Header = function Header({ title }) { + return h( + "div", + { className: "header" }, + h( + "h1", + { + style: { cursor: "pointer" }, + onClick: () => store.setView("home"), + }, + title, + ), + h("p", null, "Organization Registration & Management Portal"), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/homeView.css b/arma/client/addons/org/ui/_site/components/homeView.css new file mode 100644 index 0000000..fb1fd66 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/homeView.css @@ -0,0 +1,12 @@ +.content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 960px) { + .content { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/org/ui/_site/components/homeView.js b/arma/client/addons/org/ui/_site/components/homeView.js new file mode 100644 index 0000000..5342229 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/homeView.js @@ -0,0 +1,57 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.HomeView = function HomeView() { + return h( + "div", + { className: "content" }, + h( + "div", + { className: "card" }, + h("h2", null, "Create Organization"), + h( + "p", + null, + "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", + ), + h( + "button", + { onClick: () => store.setView("create") }, + "Register", + ), + ), + h( + "div", + { className: "card" }, + h("h2", null, "Organization Portal"), + h( + "p", + null, + "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", + ), + h("button", { onClick: () => store.setView("login") }, "Login"), + ), + h( + "div", + { className: "card", style: { gridColumn: "span 2" } }, + h("h2", null, "Organization Portal Preview"), + h( + "p", + null, + "Review the refactor direction for a player organization portal with fleet, assets, treasury, reputation, roster management, and reserved space for future modules.", + ), + h( + "button", + { + onClick: () => store.setView("portal"), + }, + "Open Portal Preview", + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/index.js b/arma/client/addons/org/ui/_site/components/index.js new file mode 100644 index 0000000..8b63cea --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/index.js @@ -0,0 +1,48 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.components = RegistryApp.components || {}; + + RegistryApp.components.App = function App() { + const Navbar = RegistryApp.componentFns.Navbar; + const Header = RegistryApp.componentFns.Header; + const HomeView = RegistryApp.componentFns.HomeView; + const LoginForm = RegistryApp.componentFns.LoginForm; + const CreateOrgForm = RegistryApp.componentFns.CreateOrgForm; + const Footer = RegistryApp.componentFns.Footer; + const PortalApp = + window.OrgPortal && window.OrgPortal.components + ? window.OrgPortal.components.App + : null; + + const view = store.getView(); + + if (view === "portal" && PortalApp) { + return h("div", null, Navbar(), PortalApp()); + } + + let mainContent; + if (view === "home") { + mainContent = HomeView(); + } else if (view === "login") { + mainContent = LoginForm(); + } else if (view === "create") { + mainContent = CreateOrgForm(); + } + + return h( + "main", + null, + Navbar(), + h( + "div", + { className: "container" }, + Header({ title: "Global Organization Network" }), + mainContent, + ), + Footer(), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/loginForm.js b/arma/client/addons/org/ui/_site/components/loginForm.js new file mode 100644 index 0000000..82e87a3 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/loginForm.js @@ -0,0 +1,76 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.LoginForm = function LoginForm() { + const handleLogin = () => { + const data = { + email: String( + document.getElementById("org-login-email")?.value || "", + ), + password: String( + document.getElementById("org-login-password")?.value || "", + ), + }; + console.log("Login Attempt:", data); + store.setView("portal"); + }; + + return h( + "div", + { + className: "card", + style: { maxWidth: "400px", margin: "0 auto" }, + }, + h("h2", null, "Organization Login"), + h( + "div", + { className: "app-form" }, + h( + "div", + null, + h("label", null, "Email"), + h("input", { + id: "org-login-email", + type: "text", + placeholder: "admin@spearnet.mil", + }), + ), + h( + "div", + null, + h("label", null, "Password"), + h("input", { + id: "org-login-password", + type: "password", + placeholder: "********", + }), + ), + h( + "div", + { className: "form-actions" }, + h( + "button", + { + type: "button", + style: { width: "100%" }, + onClick: handleLogin, + }, + "Access Authenticator", + ), + h( + "span", + { + className: "cancel-link", + onClick: () => store.setView("home"), + }, + "Cancel / Return to Main", + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/navbar.css b/arma/client/addons/org/ui/_site/components/navbar.css new file mode 100644 index 0000000..9a6478e --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/navbar.css @@ -0,0 +1,79 @@ +.app-navbar { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.app-navbar-inner { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 1rem 2rem; + box-sizing: border-box; +} + +.app-navbar-brand { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.app-navbar-kicker { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; +} + +.app-navbar-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--primary-hover); + letter-spacing: -0.025em; +} + +.app-navbar-actions { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.app-navbar-view { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + font-weight: 600; +} + +.app-close-btn { + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border); + padding: 0.5rem 1rem; + font-size: 0.85rem; + + &:hover { + background: var(--bg-surface-hover); + color: var(--primary-hover); + border-color: var(--primary); + transform: none; + box-shadow: none; + } +} + +@media (max-width: 960px) { + .app-navbar-inner { + flex-direction: column; + align-items: flex-start; + padding: 1rem 1.5rem; + } + + .app-navbar-actions { + align-items: flex-start; + } +} diff --git a/arma/client/addons/org/ui/_site/components/navbar.js b/arma/client/addons/org/ui/_site/components/navbar.js new file mode 100644 index 0000000..53a7b5a --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/navbar.js @@ -0,0 +1,70 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + function closeRegistry() { + if ( + typeof A3API !== "undefined" && + typeof A3API.SendAlert === "function" + ) { + A3API.SendAlert( + JSON.stringify({ + event: "org::close", + data: {}, + }), + ); + return; + } + + store.setView("home"); + } + + RegistryApp.componentFns.Navbar = function Navbar() { + const view = store.getView(); + const viewLabel = + view === "login" + ? "Organization Login" + : view === "create" + ? "Organization Registration" + : view === "portal" + ? "Organization Portal" + : "Entry Hub"; + const actionLabel = view === "portal" ? "Sign Out" : "Close"; + + return h( + "nav", + { className: "app-navbar" }, + h( + "div", + { className: "app-navbar-inner" }, + h( + "div", + { className: "app-navbar-brand" }, + h("span", { className: "app-navbar-kicker" }, "ORBIS"), + h( + "span", + { className: "app-navbar-title" }, + "Global Organization Network", + ), + ), + h( + "div", + { className: "app-navbar-actions" }, + h("span", { className: "app-navbar-view" }, viewLabel), + h( + "button", + { + type: "button", + className: "app-close-btn", + onClick: closeRegistry, + }, + actionLabel, + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/index.html b/arma/client/addons/org/ui/_site/index.html index cdb8685..94cdd9d 100644 --- a/arma/client/addons/org/ui/_site/index.html +++ b/arma/client/addons/org/ui/_site/index.html @@ -1,243 +1,109 @@ + + + + ORBIS - Global Organization Network + - + return fetch(path).then((response) => { + if (!response.ok) { + throw new Error("Failed to load " + path); + } - -
- -
- -
-

Organization Name

-

FACTION-001

-
-
- - -
-
+ return response.text(); + }); + } - -
- -
-
-

Overview

-
Active
-
-
-
-
- Total Members - 24 -
-
- Online Now - 8 -
-
- Org Balance - $125,000 -
-
- Reputation - 200 -
-
-
-
+ function appendStyle(css) { + const style = document.createElement("style"); + style.textContent = css; + document.head.appendChild(style); + } - -
-
-

Members Online

- 8 -
-
-
-
-
-
- John Doe - Leader -
-
-
-
-
- Jane Smith - Officer -
-
-
-
-
- Mike Johnson - Member -
-
-
-
-
- Sarah Wilson - Member -
-
-
-
-
+ function appendScript(js) { + const script = document.createElement("script"); + script.text = js; + document.head.appendChild(script); + } - -
-
-

Recent Activity

-
-
-
-
-
2m ago
-
Mike Johnson completed mission "Alpha Strike"
-
-
-
15m ago
-
Jane Smith deposited $5,000 to org bank
-
-
-
1h ago
-
New member Alex Brown joined the organization
-
-
-
2h ago
-
Organization captured territory: Zone-7
-
-
-
-
- - -
-
-

Assets

-
-
-
-
- 🏢 -
- Headquarters - Downtown -
-
-
- 🚁 -
- Helicopters - 3 units -
-
-
- 🚗 -
- Vehicles - 12 units -
-
-
- 📦 -
- Storage Units - 5 locations -
-
-
-
-
- - -
-
-

Active Missions

- 3 -
-
-
-
-
- Supply Run - High Priority -
-
Deliver supplies to northern outpost
-
-
-
-
- 65% -
-
-
-
- Recon Operation - Medium Priority -
-
Scout enemy positions in Zone-4
-
-
-
-
- 30% -
-
-
-
- Territory Defense - Low Priority -
-
Maintain control of captured zones
-
-
-
-
- 90% -
-
-
-
-
-
-
- - - + Promise.all(styleFiles.map(requestText)) + .then((styles) => { + styles.forEach(appendStyle); + return Promise.all(scriptFiles.map(requestText)); + }) + .then((scripts) => { + scripts.forEach(appendScript); + }) + .catch((error) => { + console.error( + "[Org UI] Failed to load site assets.", + error, + ); + }); + + + +
+ diff --git a/arma/client/addons/org/ui/_site/portal/actions.js b/arma/client/addons/org/ui/_site/portal/actions.js new file mode 100644 index 0000000..ac813cd --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/actions.js @@ -0,0 +1,267 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const registryStore = window.RegistryApp.store; + + class OrgPortalActions { + constructor() { + this.treasuryNoticeTimer = null; + } + + formatCurrency(value) { + return "$" + value.toLocaleString(); + } + + formatVehicleType(type) { + if (!type) { + return ""; + } + + return type.charAt(0).toUpperCase() + type.slice(1); + } + + formatAssetType(type) { + if (!type) { + return ""; + } + + return type.charAt(0).toUpperCase() + type.slice(1); + } + + getAssetReadiness() { + const total = portalData.fleet.reduce( + (sum, unit) => sum + (100 - parseInt(unit.damage, 10)), + 0, + ); + return Math.round(total / portalData.fleet.length); + } + + showTreasuryNotice(type, text) { + store.setTreasuryNotice({ type, text }); + + if (this.treasuryNoticeTimer) { + clearTimeout(this.treasuryNoticeTimer); + } + + this.treasuryNoticeTimer = setTimeout(() => { + store.setTreasuryNotice({ type: "", text: "" }); + this.treasuryNoticeTimer = null; + }, 3500); + } + + parseAmount(value) { + const amount = Number(value); + return Number.isFinite(amount) ? Math.round(amount) : 0; + } + + getInputValue(id) { + const el = document.getElementById(id); + return el ? el.value : ""; + } + + closePortal() { + if ( + typeof A3API !== "undefined" && + typeof A3API.SendAlert === "function" + ) { + A3API.SendAlert( + JSON.stringify({ + event: "org::close", + data: {}, + }), + ); + return; + } + + if (registryStore) { + registryStore.setView("home"); + } + } + + openModal(type) { + if ( + (type === "payroll" || + type === "transfer" || + type === "credit") && + !permissions.canManageTreasury() + ) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return; + } + + if (type === "disband" && !permissions.canDisbandOrg()) { + return; + } + + store.setModal({ type }); + } + + closeModal() { + store.setModal(null); + } + + removeMember(memberName) { + if (!permissions.canManageMembers()) { + return false; + } + + store.setMembers((currentMembers) => + currentMembers.filter((member) => member.name !== memberName), + ); + store.setCreditLines((currentLines) => + currentLines.filter((line) => line.member !== memberName), + ); + return true; + } + + disbandOrganization() { + if (!permissions.canDisbandOrg()) { + return false; + } + + store.setOrgDisbanded(true); + this.closeModal(); + return true; + } + + runPayroll(amountPerMember) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + const members = store.getMembers(); + const funds = store.getFunds(); + + if (members.length === 0) { + this.showTreasuryNotice( + "error", + "No members available for payroll.", + ); + return false; + } + + if (amountPerMember <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid payroll amount.", + ); + return false; + } + + const total = amountPerMember * members.length; + if (total > funds) { + this.showTreasuryNotice( + "error", + "Insufficient org funds for payroll.", + ); + return false; + } + + store.setFunds(funds - total); + this.showTreasuryNotice( + "success", + `Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`, + ); + return true; + } + + sendFundsToMember(memberName, amount) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + const funds = store.getFunds(); + + if (!memberName) { + this.showTreasuryNotice( + "error", + "Select a member to receive funds.", + ); + return false; + } + + if (amount <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid transfer amount.", + ); + return false; + } + + if (amount > funds) { + this.showTreasuryNotice( + "error", + "Insufficient org funds for this transfer.", + ); + return false; + } + + store.setFunds(funds - amount); + this.showTreasuryNotice( + "success", + `${this.formatCurrency(amount)} sent to ${memberName}.`, + ); + return true; + } + + grantCreditLine(memberName, amount) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + if (!memberName) { + this.showTreasuryNotice( + "error", + "Select a member for the credit line.", + ); + return false; + } + + if (amount <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid credit line amount.", + ); + return false; + } + + store.setCreditLines((currentLines) => { + const existingIndex = currentLines.findIndex( + (line) => line.member === memberName, + ); + if (existingIndex === -1) { + return [...currentLines, { member: memberName, amount }]; + } + + const updatedLines = [...currentLines]; + updatedLines[existingIndex] = { member: memberName, amount }; + return updatedLines; + }); + + this.showTreasuryNotice( + "success", + `Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`, + ); + return true; + } + } + + OrgPortal.actions = new OrgPortalActions(); +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/activityCard.css b/arma/client/addons/org/ui/_site/portal/components/activityCard.css new file mode 100644 index 0000000..d26dbf9 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/activityCard.css @@ -0,0 +1,32 @@ +.org-activity-row { + padding: 1rem; + border: 1px solid var(--border); + border-left: 3px solid #94a3b8; + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + border-left-color: #64748b; + } + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-activity-time { + display: inline-block; + margin-bottom: 0.35rem; + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} diff --git a/arma/client/addons/org/ui/_site/portal/components/activityCard.js b/arma/client/addons/org/ui/_site/portal/components/activityCard.js new file mode 100644 index 0000000..0de0ea8 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/activityCard.js @@ -0,0 +1,44 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.ActivityCard = function ActivityCard() { + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-6" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Command Feed"), + h( + "p", + { className: "org-panel-subtitle" }, + "Recent organization-level actions and updates.", + ), + ), + ), + h( + "div", + { className: "org-activity-list" }, + ...portalData.activity.map((item) => + h( + "article", + { className: "org-activity-row" }, + h( + "span", + { className: "org-activity-time" }, + item.time, + ), + h("p", null, item.text), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/assetsCard.js b/arma/client/addons/org/ui/_site/portal/components/assetsCard.js new file mode 100644 index 0000000..86473fc --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/assetsCard.js @@ -0,0 +1,55 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.AssetsCard = function AssetsCard() { + const SimpleStat = OrgPortal.componentFns.SimpleStat; + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-7" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Assets"), + h( + "p", + { className: "org-panel-subtitle" }, + "Inventory supplies and equipment with quantity totals.", + ), + ), + ), + h( + "div", + { className: "org-simple-list" }, + ...portalData.assets.map((asset) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + asset.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatAssetType(asset.type), + ), + SimpleStat("Quantity", asset.quantity), + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/controls.css b/arma/client/addons/org/ui/_site/portal/components/controls.css new file mode 100644 index 0000000..5ed9592 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/controls.css @@ -0,0 +1,33 @@ +.org-secondary-btn { + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border); + + &:hover { + background: var(--bg-surface-hover); + color: var(--text-main); + } +} + +.org-danger-btn { + background: #7f1d1d; + color: #fef2f2; + + &:hover { + background: #991b1b; + } +} + +.org-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + padding: 0; +} + +.org-icon { + width: 1rem; + height: 1rem; +} diff --git a/arma/client/addons/org/ui/_site/portal/components/dangerCard.css b/arma/client/addons/org/ui/_site/portal/components/dangerCard.css new file mode 100644 index 0000000..82a8d12 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/dangerCard.css @@ -0,0 +1,22 @@ +.org-danger-panel { + border-color: #fecaca; + background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); +} + +.org-danger-copy { + margin-bottom: 1rem; + + strong, + p { + display: block; + } + + p { + margin: 0.4rem 0 0; + color: var(--text-muted); + } +} + +.org-empty-state { + text-align: left; +} diff --git a/arma/client/addons/org/ui/_site/portal/components/dangerCard.js b/arma/client/addons/org/ui/_site/portal/components/dangerCard.js new file mode 100644 index 0000000..16fbf31 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/dangerCard.js @@ -0,0 +1,56 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DangerCard = function DangerCard() { + if (!permissions.canDisbandOrg()) { + return null; + } + + return h( + "section", + { className: "card org-panel org-span-12 org-danger-panel" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "h2", + { className: "org-panel-title" }, + "Organization Controls", + ), + h( + "p", + { className: "org-panel-subtitle" }, + "Leader-only actions for membership and permanent organization removal.", + ), + ), + ), + h( + "div", + { className: "org-danger-copy" }, + h("strong", null, "Disband organization"), + h( + "p", + null, + "This removes the organization and revokes access to the portal for all members.", + ), + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.openModal("disband"), + }, + "Disband Organization", + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/disbandedView.js b/arma/client/addons/org/ui/_site/portal/components/disbandedView.js new file mode 100644 index 0000000..94d4497 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/disbandedView.js @@ -0,0 +1,47 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const registryStore = window.RegistryApp.store; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DisbandedView = function DisbandedView() { + return h( + "section", + { className: "card org-panel org-span-12 org-empty-state" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "div", + { className: "org-eyebrow" }, + "Organization Removed", + ), + h( + "h2", + { className: "org-panel-title" }, + portalData.org.name, + ), + ), + ), + h( + "p", + { className: "org-summary" }, + "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => registryStore.setView("home"), + }, + "Return to Registry", + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/fleetCard.js b/arma/client/addons/org/ui/_site/portal/components/fleetCard.js new file mode 100644 index 0000000..7e69f37 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/fleetCard.js @@ -0,0 +1,56 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FleetCard = function FleetCard() { + const SimpleStat = OrgPortal.componentFns.SimpleStat; + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-7" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Fleet"), + h( + "p", + { className: "org-panel-subtitle" }, + "Individual vehicles with type, status, and overall damage.", + ), + ), + ), + h( + "div", + { className: "org-simple-list" }, + ...portalData.fleet.map((unit) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + unit.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatVehicleType(unit.type), + ), + SimpleStat("Status", unit.status), + SimpleStat("Damage", unit.damage), + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/footer.js b/arma/client/addons/org/ui/_site/portal/components/footer.js new file mode 100644 index 0000000..50454df --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/footer.js @@ -0,0 +1,43 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.Footer = function Footer() { + return h( + "div", + { className: "footer" }, + h( + "div", + { className: "wrapper" }, + h( + "div", + null, + h("h3", null, "Organization Controls"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Roster Management"), + h("li", null, "Fleet Assignment"), + h("li", null, "Treasury Permissions"), + h("li", null, "Asset Registry"), + ), + ), + h( + "div", + null, + h("h3", null, "Planned Extensions"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Contracts Board"), + h("li", null, "Diplomacy Layer"), + h("li", null, "Procurement Queue"), + h("li", null, "Reputation History"), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/futureCard.css b/arma/client/addons/org/ui/_site/portal/components/futureCard.css new file mode 100644 index 0000000..ad6407e --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/futureCard.css @@ -0,0 +1,74 @@ +.org-roadmap-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + flex: 1; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; +} + +.org-roadmap-card { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.7rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(4n + 2), + &:nth-child(4n + 3) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(100 116 139 / 0.4); + } + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-list-tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + background: #e2e8f0; + color: var(--primary-hover); + + .org-roadmap-card:nth-child(4n + 2) &, + .org-roadmap-card:nth-child(4n + 3) & { + background: #cbd5e1; + color: #1e293b; + } +} + +@media (max-width: 960px) { + .org-roadmap-grid { + grid-template-columns: 1fr; + } + + .org-roadmap-card { + &:nth-child(4n + 3) { + background: #f8fafc; + border-color: var(--border); + } + } + + .org-list-tag { + .org-roadmap-card:nth-child(4n + 3) & { + background: #e2e8f0; + color: var(--primary-hover); + } + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/futureCard.js b/arma/client/addons/org/ui/_site/portal/components/futureCard.js new file mode 100644 index 0000000..72b7158 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/futureCard.js @@ -0,0 +1,45 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FutureCard = function FutureCard() { + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-6" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "h2", + { className: "org-panel-title" }, + "Expansion Slots", + ), + h( + "p", + { className: "org-panel-subtitle" }, + "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", + ), + ), + ), + h( + "div", + { className: "org-roadmap-grid" }, + ...portalData.roadmap.map((item) => + h( + "article", + { className: "org-roadmap-card" }, + h("span", { className: "org-list-tag" }, item.status), + h("strong", null, item.name), + h("p", null, item.detail), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/index.js b/arma/client/addons/org/ui/_site/portal/components/index.js new file mode 100644 index 0000000..d6a80e9 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/index.js @@ -0,0 +1,65 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const store = OrgPortal.store; + + OrgPortal.components = OrgPortal.components || {}; + + OrgPortal.components.App = function App() { + const PortalHeader = OrgPortal.componentFns.PortalHeader; + const OverviewCard = OrgPortal.componentFns.OverviewCard; + const FleetCard = OrgPortal.componentFns.FleetCard; + const TreasuryCard = OrgPortal.componentFns.TreasuryCard; + const MembersCard = OrgPortal.componentFns.MembersCard; + const AssetsCard = OrgPortal.componentFns.AssetsCard; + const ActivityCard = OrgPortal.componentFns.ActivityCard; + const FutureCard = OrgPortal.componentFns.FutureCard; + const DangerCard = OrgPortal.componentFns.DangerCard; + const ModalLayer = OrgPortal.componentFns.ModalLayer; + const DisbandedView = OrgPortal.componentFns.DisbandedView; + const Footer = OrgPortal.componentFns.Footer; + + if (store.getOrgDisbanded()) { + return h( + "main", + null, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + PortalHeader(), + DisbandedView(), + ), + ), + ModalLayer(), + Footer(), + ); + } + + return h( + "main", + null, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + PortalHeader(), + OverviewCard(), + FleetCard(), + TreasuryCard(), + MembersCard(), + AssetsCard(), + ActivityCard(), + FutureCard(), + DangerCard(), + ), + ), + ModalLayer(), + Footer(), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/layout.css b/arma/client/addons/org/ui/_site/portal/components/layout.css new file mode 100644 index 0000000..b46b675 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/layout.css @@ -0,0 +1,81 @@ +.org-dashboard-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 1.5rem; +} + +.org-panel { + margin-bottom: 0; + text-align: left; +} + +.org-scroll-panel { + display: flex; + flex-direction: column; + max-height: 31rem; + overflow: hidden; +} + +.org-span-12 { + grid-column: span 12; +} + +.org-span-7 { + grid-column: span 7; +} + +.org-span-6 { + grid-column: span 6; +} + +.org-span-5 { + grid-column: span 5; +} + +.org-panel-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; + + &.org-panel-head-stack { + flex-direction: column; + align-items: stretch; + } +} + +.org-eyebrow { + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.4rem; +} + +.org-panel-title { + margin: 0; + color: var(--primary-hover); + font-size: 1.45rem; +} + +.org-panel-subtitle { + margin: 0.35rem 0 0; + color: var(--text-muted); + font-size: 0.95rem; +} + +@media (max-width: 960px) { + .org-span-12, + .org-span-7, + .org-span-6, + .org-span-5 { + grid-column: span 12; + } + + .org-panel-head { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/membersCard.js b/arma/client/addons/org/ui/_site/portal/components/membersCard.js new file mode 100644 index 0000000..8333b7c --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/membersCard.js @@ -0,0 +1,75 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.MembersCard = function MembersCard() { + const members = store.getMembers(); + const allowMemberManagement = permissions.canManageMembers(); + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-5" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Members"), + h( + "p", + { className: "org-panel-subtitle" }, + "Current roster listing with member removal controls.", + ), + ), + ), + h( + "div", + { className: "org-name-list" }, + ...members.map((member) => + h( + "article", + { className: "org-name-row" }, + h("strong", null, member.name), + allowMemberManagement + ? h( + "button", + { + type: "button", + className: "org-danger-btn org-icon-btn", + title: `Remove ${member.name}`, + "aria-label": `Remove ${member.name}`, + onClick: () => + actions.removeMember(member.name), + }, + h( + "svg", + { + className: "org-icon", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "aria-hidden": "true", + }, + h("path", { d: "M9 3h6" }), + h("path", { d: "M4 7h16" }), + h("path", { d: "M6 7l1 13h10l1-13" }), + h("path", { d: "M10 11v6" }), + h("path", { d: "M14 11v6" }), + ), + ) + : null, + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/metricCard.css b/arma/client/addons/org/ui/_site/portal/components/metricCard.css new file mode 100644 index 0000000..b7d283f --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/metricCard.css @@ -0,0 +1,69 @@ +.org-metric-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.org-metric-card { + display: flex; + flex-direction: column; + gap: 0.45rem; + padding: 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + + &:nth-child(4n + 2), + &:nth-child(4n + 3) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(226 232 240) 100% + ); + border-color: rgb(100 116 139 / 0.35); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); + } +} + +.org-metric-label { + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.org-metric-value { + font-size: 1.8rem; + color: var(--primary-hover); + line-height: 1.1; + + .org-metric-card:nth-child(4n + 2) &, + .org-metric-card:nth-child(4n + 3) & { + color: #334155; + } +} + +.org-metric-note { + color: var(--text-muted); + font-size: 0.9rem; +} + +@media (max-width: 960px) { + .org-metric-grid { + grid-template-columns: 1fr; + } + + .org-metric-card { + &:nth-child(4n + 3) { + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + border-color: var(--border); + box-shadow: none; + } + } + + .org-metric-value { + .org-metric-card:nth-child(4n + 3) & { + color: var(--primary-hover); + } + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/metricCard.js b/arma/client/addons/org/ui/_site/portal/components/metricCard.js new file mode 100644 index 0000000..065fe94 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/metricCard.js @@ -0,0 +1,20 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.MetricCard = function MetricCard( + label, + value, + note, + ) { + return h( + "div", + { className: "org-metric-card" }, + h("span", { className: "org-metric-label" }, label), + h("strong", { className: "org-metric-value" }, value), + h("span", { className: "org-metric-note" }, note), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/modalLayer.css b/arma/client/addons/org/ui/_site/portal/components/modalLayer.css new file mode 100644 index 0000000..1b2a41d --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/modalLayer.css @@ -0,0 +1,131 @@ +.org-modal-backdrop { + position: fixed; + inset: 0; + background: rgb(15 23 42 / 0.38); + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + z-index: 20; +} + +.org-modal-card { + width: min(100%, 30rem); + margin-bottom: 0; + text-align: left; +} + +.org-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.org-modal-close { + width: 2.25rem; + height: 2.25rem; + padding: 0; + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border); + box-shadow: none; + transform: none; + + &:hover { + background: var(--bg-surface-hover); + color: var(--text-main); + box-shadow: none; + transform: none; + } +} + +.org-modal-form { + display: flex; + flex-direction: column; + gap: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; + } + + input, + select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: + border-color 0.2s, + box-shadow 0.2s; + + &:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); + } + + &:disabled { + background: #f1f5f9; + color: var(--text-muted); + cursor: not-allowed; + } + } +} + +.org-modal-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; + + button + button { + margin-left: 0; + } +} + +.org-danger-confirm { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid #fecaca; + border-radius: var(--radius); + background: #fff1f2; + align-items: flex-start; + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-danger-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + + button + button { + margin-left: 0; + } +} + +@media (max-width: 960px) { + .org-modal-head, + .org-danger-confirm { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/modalLayer.js b/arma/client/addons/org/ui/_site/portal/components/modalLayer.js new file mode 100644 index 0000000..af566ad --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/modalLayer.js @@ -0,0 +1,286 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.ModalLayer = function ModalLayer() { + const modal = store.getModal(); + if (!modal) { + return null; + } + + const members = store.getMembers(); + const memberSelectProps = + members.length === 0 ? { disabled: true } : {}; + + let title = ""; + let body = null; + + if (modal.type === "payroll") { + title = "Run Payroll"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Amount Per Member"), + h("input", { + id: "treasury-payroll-amount", + type: "number", + min: "1", + placeholder: "500", + autofocus: "true", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + onClick: () => { + if ( + actions.runPayroll( + actions.parseAmount( + actions.getInputValue( + "treasury-payroll-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Run Payroll", + ), + ), + ); + } else if (modal.type === "transfer") { + title = "Send Funds"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Member"), + h( + "select", + { + id: "treasury-transfer-member", + ...memberSelectProps, + }, + ...members.map((member) => + h("option", { value: member.name }, member.name), + ), + ), + ), + h( + "div", + null, + h("label", null, "Amount"), + h("input", { + id: "treasury-transfer-amount", + type: "number", + min: "1", + placeholder: "1500", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + ...memberSelectProps, + onClick: () => { + if ( + actions.sendFundsToMember( + String( + actions.getInputValue( + "treasury-transfer-member", + ) || "", + ), + actions.parseAmount( + actions.getInputValue( + "treasury-transfer-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Send Funds", + ), + ), + ); + } else if (modal.type === "credit") { + title = "Assign Credit Line"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Member"), + h( + "select", + { id: "treasury-credit-member", ...memberSelectProps }, + ...members.map((member) => + h("option", { value: member.name }, member.name), + ), + ), + ), + h( + "div", + null, + h("label", null, "Credit Amount"), + h("input", { + id: "treasury-credit-amount", + type: "number", + min: "1", + placeholder: "5000", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + ...memberSelectProps, + onClick: () => { + if ( + actions.grantCreditLine( + String( + actions.getInputValue( + "treasury-credit-member", + ) || "", + ), + actions.parseAmount( + actions.getInputValue( + "treasury-credit-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Assign Credit Line", + ), + ), + ); + } else if (modal.type === "disband") { + title = "Disband Organization"; + body = h( + "div", + { className: "org-danger-confirm" }, + h( + "p", + null, + "This action is permanent. Disband ", + portalData.org.name, + "?", + ), + h( + "div", + { className: "org-danger-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.disbandOrganization(), + }, + "Confirm Disband", + ), + ), + ); + } + + return h( + "div", + { + className: "org-modal-backdrop", + onClick: (e) => { + if (e.target === e.currentTarget) { + actions.closeModal(); + } + }, + }, + h( + "div", + { className: "card org-modal-card" }, + h( + "div", + { className: "org-modal-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, title), + ), + h( + "button", + { + type: "button", + className: "org-modal-close", + onClick: () => actions.closeModal(), + "aria-label": "Close dialog", + }, + "x", + ), + ), + body, + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/overviewCard.css b/arma/client/addons/org/ui/_site/portal/components/overviewCard.css new file mode 100644 index 0000000..e961dde --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/overviewCard.css @@ -0,0 +1,58 @@ +.org-hero-grid { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 1.5rem; + align-items: start; +} + +.org-summary { + margin: 0; + font-size: 1.05rem; + color: var(--text-main); +} + +.org-meta-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.org-meta-item { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(241 245 249) 0%, + rgb(226 232 240) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +.org-meta-label { + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.org-meta-value { + font-size: 1rem; + font-weight: 600; + color: var(--primary-hover); +} + +@media (max-width: 960px) { + .org-hero-grid, + .org-meta-row { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/overviewCard.js b/arma/client/addons/org/ui/_site/portal/components/overviewCard.js new file mode 100644 index 0000000..f8aa4b3 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/overviewCard.js @@ -0,0 +1,118 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.OverviewCard = function OverviewCard() { + const MetricCard = OrgPortal.componentFns.MetricCard; + + return h( + "section", + { className: "card org-panel org-span-12" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("div", { className: "org-eyebrow" }, portalData.org.tag), + h( + "h2", + { className: "org-panel-title" }, + "Organization Overview", + ), + ), + ), + h( + "div", + { className: "org-hero-grid" }, + h( + "div", + { className: "org-hero-copy" }, + h( + "p", + { className: "org-summary" }, + portalData.org.type, + " operating from ", + portalData.org.headquarters, + ". Treasury, fleet status, inventory, and roster management are surfaced here first.", + ), + h( + "div", + { className: "org-meta-row" }, + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Director", + ), + h( + "span", + { className: "org-meta-value" }, + portalData.org.owner, + ), + ), + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Active Members", + ), + h( + "span", + { className: "org-meta-value" }, + `${store.getMembers().length} total`, + ), + ), + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Fleet Readiness", + ), + h( + "span", + { className: "org-meta-value" }, + `${actions.getAssetReadiness()}%`, + ), + ), + ), + ), + h( + "div", + { className: "org-metric-grid" }, + MetricCard( + "Org Funds", + actions.formatCurrency(store.getFunds()), + "Organization treasury balance", + ), + MetricCard( + "Reputation", + portalData.reputation, + "Organization standing", + ), + MetricCard( + "Asset Lines", + portalData.assets.length, + "Tracked supply and equipment entries", + ), + MetricCard( + "Fleet Vehicles", + portalData.fleet.length, + "Tracked air, ground, and naval vehicles", + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/portalHeader.css b/arma/client/addons/org/ui/_site/portal/components/portalHeader.css new file mode 100644 index 0000000..eaafcae --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/portalHeader.css @@ -0,0 +1,41 @@ +.org-page-header { + text-align: left; + margin-bottom: 0; +} + +.org-page-heading { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.org-page-kicker { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; +} + +.org-page-title { + margin: 0; +} + +.org-page-subtitle { + font-size: 0.9rem; + color: var(--text-muted); + margin: 0; +} + +.org-page-meta { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@media (max-width: 960px) { + .org-page-heading { + gap: 0.3rem; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/portalHeader.js b/arma/client/addons/org/ui/_site/portal/components/portalHeader.js new file mode 100644 index 0000000..9ec5cf0 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/portalHeader.js @@ -0,0 +1,30 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData, session } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.PortalHeader = function PortalHeader() { + return h( + "section", + { className: "card org-panel org-span-12 org-page-header" }, + h( + "div", + { className: "org-page-heading" }, + h("span", { className: "org-page-kicker" }, portalData.org.tag), + h("h1", { className: "org-page-title" }, portalData.org.name), + h( + "p", + { className: "org-page-subtitle" }, + "Player organization command portal", + ), + h( + "span", + { className: "org-page-meta" }, + `${session.actorName} - ${session.role}`, + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/simpleList.css b/arma/client/addons/org/ui/_site/portal/components/simpleList.css new file mode 100644 index 0000000..be49ccc --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/simpleList.css @@ -0,0 +1,67 @@ +.org-simple-list, +.org-name-list, +.org-activity-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; +} + +.org-simple-list, +.org-name-list, +.org-activity-list, +.org-roadmap-grid { + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +.org-simple-row, +.org-name-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +.org-simple-name { + color: var(--primary-hover); +} + +.org-simple-meta { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 1rem; +} + +.org-name-row { + justify-content: flex-start; + + button { + margin-left: auto; + } +} + +@media (max-width: 960px) { + .org-simple-row, + .org-name-row { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/simpleStat.css b/arma/client/addons/org/ui/_site/portal/components/simpleStat.css new file mode 100644 index 0000000..824ad98 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/simpleStat.css @@ -0,0 +1,18 @@ +.org-simple-stat { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 90px; +} + +.org-simple-label { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.org-simple-value { + font-size: 0.95rem; + color: var(--text-main); +} diff --git a/arma/client/addons/org/ui/_site/portal/components/simpleStat.js b/arma/client/addons/org/ui/_site/portal/components/simpleStat.js new file mode 100644 index 0000000..9922ac8 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/simpleStat.js @@ -0,0 +1,15 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { + return h( + "div", + { className: "org-simple-stat" }, + h("span", { className: "org-simple-label" }, label), + h("strong", { className: "org-simple-value" }, value), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/treasuryCard.css b/arma/client/addons/org/ui/_site/portal/components/treasuryCard.css new file mode 100644 index 0000000..26a4417 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/treasuryCard.css @@ -0,0 +1,99 @@ +.org-finance-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; + + > div { + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + display: flex; + flex-direction: column; + gap: 0.4rem; + } +} + +.org-action-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; + + button + button { + margin-left: 0; + } + + button { + width: 100%; + } +} + +.org-treasury-notice { + margin-bottom: 1rem; + padding: 0.85rem 1rem; + border-radius: var(--radius); + font-size: 0.92rem; + + &.is-success { + background: #ecfdf5; + border: 1px solid #bbf7d0; + color: #166534; + } + + &.is-error { + background: #fef2f2; + border: 1px solid #fecaca; + color: #991b1b; + } +} + +.org-credit-lines { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.org-access-note { + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.95rem; +} + +.org-credit-list { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.org-credit-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +@media (max-width: 960px) { + .org-finance-meta { + grid-template-columns: 1fr; + } + + .org-credit-row { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/client/addons/org/ui/_site/portal/components/treasuryCard.js b/arma/client/addons/org/ui/_site/portal/components/treasuryCard.js new file mode 100644 index 0000000..a280783 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/components/treasuryCard.js @@ -0,0 +1,130 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { + const notice = store.getTreasuryNotice(); + const creditLines = store.getCreditLines(); + const noMembers = store.getMembers().length === 0; + const allowTreasuryActions = permissions.canManageTreasury(); + + return h( + "section", + { className: "card org-panel org-span-5" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Treasury"), + h( + "p", + { className: "org-panel-subtitle" }, + "Organization funds, reputation, and member payouts.", + ), + ), + ), + h( + "div", + { className: "org-finance-meta" }, + h( + "div", + null, + h("span", { className: "org-meta-label" }, "Funds"), + h("strong", null, actions.formatCurrency(store.getFunds())), + ), + h( + "div", + null, + h("span", { className: "org-meta-label" }, "Reputation"), + h("strong", null, `${portalData.reputation}`), + ), + ), + allowTreasuryActions + ? h( + "div", + { className: "org-action-grid" }, + h( + "button", + { + type: "button", + onClick: () => actions.openModal("payroll"), + disabled: noMembers, + }, + "Run Payroll", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.openModal("transfer"), + disabled: noMembers, + }, + "Send Funds", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.openModal("credit"), + disabled: noMembers, + }, + "Credit Line", + ), + ) + : h( + "p", + { className: "org-access-note" }, + "Only the organization leader or CEO can manage treasury actions.", + ), + notice.text + ? h( + "div", + { + className: + notice.type === "error" + ? "org-treasury-notice is-error" + : "org-treasury-notice is-success", + }, + notice.text, + ) + : null, + creditLines.length > 0 + ? h( + "div", + { className: "org-credit-lines" }, + h( + "span", + { className: "org-meta-label" }, + "Active Credit Lines", + ), + h( + "div", + { className: "org-credit-list" }, + ...creditLines.map((line) => + h( + "div", + { className: "org-credit-row" }, + h("span", null, line.member), + h( + "strong", + null, + actions.formatCurrency(line.amount), + ), + ), + ), + ), + ) + : null, + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/data.js b/arma/client/addons/org/ui/_site/portal/data.js new file mode 100644 index 0000000..484aaa0 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/data.js @@ -0,0 +1,118 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + + OrgPortal.data = { + portalData: { + org: { + name: "Black Rifle Company", + tag: "BRC-0160566824", + type: "Private Military Company", + status: "Operational", + headquarters: "Georgetown Command Annex", + owner: "Jacob Schmidt", + }, + funds: 482750, + reputation: 72, + members: [ + { name: "Jacob Schmidt" }, + { name: "Mara Velez" }, + { name: "Rylan Cross" }, + { name: "Noah Briggs" }, + { name: "Elena Price" }, + { name: "Isaac Rowe" }, + { name: "Talia Boone" }, + { name: "Cade Mercer" }, + ], + fleet: [ + { + name: "UH-80 Ghost Hawk", + type: "helicopter", + status: "Ready", + damage: "16%", + }, + { + name: "MH-9 Hummingbird", + type: "helicopter", + status: "Ready", + damage: "8%", + }, + { + name: "M-ATV Patrol 1", + type: "car", + status: "Fielded", + damage: "24%", + }, + { + name: "M2A1 Slammer", + type: "armor", + status: "Ready", + damage: "11%", + }, + { + name: "RHIB Patrol Boat", + type: "naval", + status: "Repairing", + damage: "32%", + }, + ], + assets: [ + { name: "First Aid Kits", type: "items", quantity: "36" }, + { name: "MX 6.5 mm Rifles", type: "weapons", quantity: "18" }, + { + name: "6.5 mm Magazines", + type: "magazines", + quantity: "120", + }, + { + name: "Carryall Backpacks", + type: "backpacks", + quantity: "24", + }, + ], + activity: [ + { + time: "08:20", + text: "Treasury cleared contractor payment for northern route escort.", + }, + { + time: "07:45", + text: "Viper Flight completed readiness checks on all rotary assets.", + }, + { + time: "07:10", + text: "New recruit Cade Mercer accepted into ground training roster.", + }, + { + time: "06:30", + text: "North Depot inventory count pushed reserve ratio above target.", + }, + ], + roadmap: [ + { + name: "Contracts Board", + status: "Planned", + detail: "Track payouts, assignments, and claim approvals.", + }, + { + name: "Diplomacy", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Logistics Queue", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Permissions", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + ], + }, + session: { + actorName: "Jacob Schmidt", + role: "Leader", + }, + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/permissions.js b/arma/client/addons/org/ui/_site/portal/permissions.js new file mode 100644 index 0000000..7de691e --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/permissions.js @@ -0,0 +1,28 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { portalData, session } = OrgPortal.data; + + class OrgPortalPermissions { + isOrgLeaderOrCeo() { + return ( + session.actorName === portalData.org.owner || + session.role === "Leader" || + session.role === "CEO" + ); + } + + canManageMembers() { + return this.isOrgLeaderOrCeo(); + } + + canManageTreasury() { + return this.isOrgLeaderOrCeo(); + } + + canDisbandOrg() { + return this.isOrgLeaderOrCeo(); + } + } + + OrgPortal.permissions = new OrgPortalPermissions(); +})(); diff --git a/arma/client/addons/org/ui/_site/portal/runtime.js b/arma/client/addons/org/ui/_site/portal/runtime.js new file mode 100644 index 0000000..8798b3b --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/runtime.js @@ -0,0 +1,98 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + + const SVG_NS = "http://www.w3.org/2000/svg"; + const SVG_TAGS = new Set([ + "svg", + "path", + "circle", + "rect", + "line", + "polyline", + "polygon", + "g", + "defs", + "use", + "text", + "tspan", + "clipPath", + "mask", + ]); + + function h(tag, props = {}, ...children) { + const isSvg = SVG_TAGS.has(tag); + const el = isSvg + ? document.createElementNS(SVG_NS, tag) + : document.createElement(tag); + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith("on") && typeof value === "function") { + el.addEventListener(key.substring(2).toLowerCase(), value); + } else if (key === "className") { + if (isSvg) { + el.setAttribute("class", value); + } else { + el.className = value; + } + } else if (key === "style" && typeof value === "object") { + Object.assign(el.style, value); + } else if (typeof value === "boolean") { + if (value) { + el.setAttribute(key, ""); + } else { + el.removeAttribute(key); + } + } else if (value === null || value === undefined) { + el.removeAttribute(key); + } else { + el.setAttribute(key, value); + } + }); + } + + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { + el.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + el.appendChild(child); + } else if (Array.isArray(child)) { + child.forEach((c) => el.appendChild(c)); + } + }); + + return el; + } + + let rootContainer = null; + let rootComponent = null; + + function render(component, container) { + rootContainer = container; + rootComponent = component; + rerender(); + } + + function rerender() { + rootContainer.innerHTML = ""; + rootContainer.appendChild(rootComponent()); + } + + function createSignal(initialValue) { + let value = initialValue; + + const getValue = () => value; + const setValue = (newValue) => { + value = typeof newValue === "function" ? newValue(value) : newValue; + rerender(); + }; + + return [getValue, setValue]; + } + + OrgPortal.runtime = { + h, + render, + createSignal, + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/store.js b/arma/client/addons/org/ui/_site/portal/store.js new file mode 100644 index 0000000..cb882b6 --- /dev/null +++ b/arma/client/addons/org/ui/_site/portal/store.js @@ -0,0 +1,23 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { createSignal } = window.RegistryApp.runtime; + const { portalData } = OrgPortal.data; + + class OrgPortalStore { + constructor() { + [this.getFunds, this.setFunds] = createSignal(portalData.funds); + [this.getMembers, this.setMembers] = createSignal([ + ...portalData.members, + ]); + [this.getCreditLines, this.setCreditLines] = createSignal([]); + [this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({ + type: "", + text: "", + }); + [this.getModal, this.setModal] = createSignal(null); + [this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false); + } + } + + OrgPortal.store = new OrgPortalStore(); +})(); diff --git a/arma/client/addons/org/ui/_site/runtime.js b/arma/client/addons/org/ui/_site/runtime.js new file mode 100644 index 0000000..70f267b --- /dev/null +++ b/arma/client/addons/org/ui/_site/runtime.js @@ -0,0 +1,73 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + + function h(tag, props = {}, ...children) { + const el = document.createElement(tag); + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith("on") && typeof value === "function") { + el.addEventListener(key.substring(2).toLowerCase(), value); + } else if (key === "className") { + el.className = value; + } else if (key === "style" && typeof value === "object") { + Object.assign(el.style, value); + } else if (typeof value === "boolean") { + if (value) { + el.setAttribute(key, ""); + } else { + el.removeAttribute(key); + } + } else if (value === null || value === undefined) { + el.removeAttribute(key); + } else { + el.setAttribute(key, value); + } + }); + } + + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { + el.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + el.appendChild(child); + } else if (Array.isArray(child)) { + child.forEach((c) => el.appendChild(c)); + } + }); + + return el; + } + + let rootContainer = null; + let rootComponent = null; + + function render(component, container) { + rootContainer = container; + rootComponent = component; + rerender(); + } + + function rerender() { + rootContainer.innerHTML = ""; + rootContainer.appendChild(rootComponent()); + } + + function createSignal(initialValue) { + let value = initialValue; + + const getValue = () => value; + const setValue = (newValue) => { + value = typeof newValue === "function" ? newValue(value) : newValue; + rerender(); + }; + + return [getValue, setValue]; + } + + RegistryApp.runtime = { + h, + render, + createSignal, + }; +})(); diff --git a/arma/client/addons/org/ui/_site/script.js b/arma/client/addons/org/ui/_site/script.js deleted file mode 100644 index d6561e9..0000000 --- a/arma/client/addons/org/ui/_site/script.js +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Organization Dashboard - * Handles real-time updates and interactions - */ - -// Mock data for demonstration -const mockData = { - org: { - name: "Black Phoenix Initiative", - tag: "BPI-001", - status: "Active" - }, - stats: { - totalMembers: 24, - onlineMembers: 8, - balance: 125000, - reputation: 5 - }, - membersOnline: [ - { name: "John Doe", rank: "Leader", online: true }, - { name: "Jane Smith", rank: "Officer", online: true }, - { name: "Mike Johnson", rank: "Member", online: true }, - { name: "Sarah Wilson", rank: "Member", online: true }, - { name: "Alex Brown", rank: "Member", online: true }, - { name: "Chris Davis", rank: "Member", online: true }, - { name: "Pat Lee", rank: "Recruit", online: true }, - { name: "Sam Taylor", rank: "Recruit", online: true } - ], - activities: [ - { time: "2m ago", text: "Mike Johnson completed mission \"Alpha Strike\"" }, - { time: "15m ago", text: "Jane Smith deposited $5,000 to org bank" }, - { time: "1h ago", text: "New member Alex Brown joined the organization" }, - { time: "2h ago", text: "Organization captured territory: Zone-7" } - ], - assets: [ - { icon: "🏢", name: "Headquarters", location: "Downtown" }, - { icon: "🚁", name: "Helicopters", location: "3 units" }, - { icon: "🚗", name: "Vehicles", location: "12 units" }, - { icon: "📦", name: "Storage Units", location: "5 locations" } - ], - missions: [ - { - name: "Supply Run", - priority: "high", - description: "Deliver supplies to northern outpost", - progress: 65 - }, - { - name: "Recon Operation", - priority: "medium", - description: "Scout enemy positions in Zone-4", - progress: 30 - }, - { - name: "Territory Defense", - priority: "low", - description: "Maintain control of captured zones", - progress: 90 - } - ] -}; - -// Update dashboard with data -function updateDashboard(data) { - // Update header - if (data.org) { - const orgName = document.querySelector('.org-name'); - const orgTag = document.querySelector('.org-tag'); - if (orgName) orgName.textContent = data.org.name; - if (orgTag) orgTag.textContent = data.org.tag; - } - - // Update stats - if (data.stats) { - const statValues = document.querySelectorAll('.stat-value'); - if (statValues[0]) statValues[0].textContent = data.stats.totalMembers; - if (statValues[1]) statValues[1].textContent = data.stats.onlineMembers; - if (statValues[2]) statValues[2].textContent = `$${data.stats.balance.toLocaleString()}`; - if (statValues[3]) statValues[3].textContent = `${data.stats.reputation}`; - } - - // Update Members List - if (data.membersOnline) { - const memberList = document.querySelector('.member-list'); - if (memberList) { - memberList.innerHTML = ''; - data.membersOnline.forEach(member => { - const item = document.createElement('div'); - item.className = 'member-item'; - item.innerHTML = ` -
-
- ${member.name} - ${member.rank} -
- `; - memberList.appendChild(item); - }); - - // Update member count badge - const memberBadge = document.querySelector('.dashboard-card:nth-child(2) .card-badge'); - if (memberBadge) memberBadge.textContent = data.membersOnline.length; - } - } - - // Update Assets List - if (data.assets) { - const assetList = document.querySelector('.asset-list'); - if (assetList) { - assetList.innerHTML = ''; - data.assets.forEach(asset => { - const item = document.createElement('div'); - item.className = 'asset-item'; - item.innerHTML = ` - ${asset.icon || '📦'} -
- ${asset.name} - ${asset.location} -
- `; - assetList.appendChild(item); - }); - } - } - - // Update Activities List - if (data.activities) { - const activityList = document.querySelector('.activity-list'); - if (activityList) { - activityList.innerHTML = ''; - data.activities.forEach(activity => { - const item = document.createElement('div'); - item.className = 'activity-item'; - item.innerHTML = ` -
${activity.time}
-
${activity.text}
- `; - activityList.appendChild(item); - }); - } - } - - // Update Missions List - if (data.missions) { - const missionList = document.querySelector('.mission-list'); - if (missionList) { - missionList.innerHTML = ''; - data.missions.forEach(mission => { - const item = document.createElement('div'); - item.className = 'mission-item'; - item.innerHTML = ` -
- ${mission.name} - ${mission.priority.charAt(0).toUpperCase() + mission.priority.slice(1)} Priority -
-
${mission.description}
-
-
-
-
- ${mission.progress}% -
- `; - missionList.appendChild(item); - }); - - // Update mission count badge - const missionBadge = document.querySelector('.dashboard-card:last-child .card-badge'); - if (missionBadge) missionBadge.textContent = data.missions.length; - } - } - - // Re-attach event handlers for new elements if needed - // Note: The original setupEventHandlers attached to querySelectorAll which only finds elements existing at that time. - // We should re-run or use delegation. For simplicity, we can re-attach listeners to the new items. - attachDynamicHandlers(data); -} - -function attachDynamicHandlers(data) { - // Member item clicks - const memberItems = document.querySelectorAll('.member-item'); - memberItems.forEach((item, index) => { - item.addEventListener('click', () => { - const memberData = data.membersOnline ? data.membersOnline[index] : null; - if (memberData) { - console.log('Member clicked:', memberData); - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::member::view', - data: { member: memberData } - })); - } - } - }); - }); - - // Asset item clicks - const assetItems = document.querySelectorAll('.asset-item'); - assetItems.forEach((item, index) => { - item.addEventListener('click', () => { - const assetData = data.assets ? data.assets[index] : null; - if (assetData) { - console.log('Asset clicked:', assetData); - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::asset::view', - data: { asset: assetData } - })); - } - } - }); - }); - - // Mission item clicks - const missionItems = document.querySelectorAll('.mission-item'); - missionItems.forEach((item, index) => { - item.addEventListener('click', () => { - const missionData = data.missions ? data.missions[index] : null; - if (missionData) { - console.log('Mission clicked:', missionData); - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::mission::view', - data: { mission: missionData } - })); - } - } - }); - }); -} - -// Static Event Handlers (Close, Settings, etc.) -function setupEventHandlers() { - // Close button - const closeBtn = document.querySelector('.close-btn'); - if (closeBtn) { - closeBtn.addEventListener('click', () => { - console.log('Closing dashboard...'); - // Send close event to Arma - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::close', - data: {} - })); - } - }); - } - - // Settings button - const settingsBtn = document.querySelectorAll('.action-btn')[0]; - if (settingsBtn && !settingsBtn.classList.contains('close-btn')) { - settingsBtn.addEventListener('click', () => { - console.log('Opening settings...'); - // Send settings event to Arma - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::settings', - data: {} - })); - } - }); - } -} - -// Initialize dashboard -function initDashboard() { - console.log('Organization Dashboard initializing...'); - - // Setup event handlers - setupEventHandlers(); - - // Request live data from Arma - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: 'org::ready', - data: {} - })); - } else { - // Use mock data for browser testing/development - updateDashboard(mockData); - } - - console.log('Organization Dashboard initialized'); -} - -// Auto-initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initDashboard); -} else { - initDashboard(); -} - -// Expose functions globally for Arma integration -window.updateOrgDashboard = updateDashboard; diff --git a/arma/client/addons/org/ui/_site/state.js b/arma/client/addons/org/ui/_site/state.js new file mode 100644 index 0000000..bb52993 --- /dev/null +++ b/arma/client/addons/org/ui/_site/state.js @@ -0,0 +1,12 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { createSignal } = RegistryApp.runtime; + + class RegistryStore { + constructor() { + [this.getView, this.setView] = createSignal("home"); + } + } + + RegistryApp.store = new RegistryStore(); +})(); diff --git a/arma/client/addons/org/ui/_site/style.css b/arma/client/addons/org/ui/_site/style.css deleted file mode 100644 index a4f93c1..0000000 --- a/arma/client/addons/org/ui/_site/style.css +++ /dev/null @@ -1,469 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - height: 100vh; - width: 100vw; - background: rgba(0, 0, 0, 0.6); - font-family: Arial, sans-serif; - color: rgba(200, 220, 240, 0.95); - overflow: hidden; -} - -.dashboard-container { - height: 100vh; - width: 100vw; - padding: 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -/* Header Section */ -.dashboard-header { - display: flex; - align-items: center; - gap: 1.5rem; - padding: 1.25rem 1.5rem; - background: rgba(15, 20, 30, 0.9); - border: 1px solid rgba(100, 150, 200, 0.4); - border-radius: 4px; - box-shadow: - 0 0 20px rgba(100, 150, 200, 0.15), - 0 4px 16px rgba(0, 0, 0, 0.8); -} - -.org-logo { - width: 60px; - height: 60px; - background: rgba(20, 30, 45, 0.8); - border: 2px solid rgba(100, 150, 200, 0.5); - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; -} - -.logo-placeholder { - font-size: 1.5rem; - font-weight: bold; - color: rgba(100, 150, 200, 0.9); -} - -.org-info { - flex: 1; -} - -.org-name { - font-size: 1.5rem; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; - color: rgba(200, 220, 255, 1); - margin-bottom: 0.25rem; -} - -.org-tag { - font-size: 0.875rem; - color: rgba(140, 160, 180, 0.8); - letter-spacing: 1px; -} - -.header-actions { - display: flex; - gap: 0.75rem; -} - -.action-btn { - padding: 0.625rem 1.25rem; - background: rgba(20, 30, 45, 0.7); - border: 1px solid rgba(100, 150, 200, 0.4); - border-radius: 4px; - color: rgba(200, 220, 240, 0.95); - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.5px; - cursor: pointer; - transition: all 0.15s ease; -} - -.action-btn:hover { - background: rgba(30, 45, 70, 0.9); - border-color: rgba(150, 200, 255, 0.7); - box-shadow: - 0 0 15px rgba(100, 150, 200, 0.2), - inset 0 0 20px rgba(100, 150, 200, 0.05); -} - -.close-btn { - border-color: rgba(200, 100, 100, 0.4); -} - -.close-btn:hover { - border-color: rgba(255, 100, 100, 0.7); - box-shadow: - 0 0 15px rgba(200, 100, 100, 0.2), - inset 0 0 20px rgba(200, 100, 100, 0.05); -} - -/* Dashboard Grid */ -.dashboard-grid { - flex: 1; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.25rem; - overflow-y: auto; - padding-right: 0.5rem; -} - -/* Custom Scrollbar */ -.dashboard-grid::-webkit-scrollbar { - width: 8px; -} - -.dashboard-grid::-webkit-scrollbar-track { - background: rgba(15, 20, 30, 0.5); - border-radius: 4px; -} - -.dashboard-grid::-webkit-scrollbar-thumb { - background: rgba(100, 150, 200, 0.3); - border-radius: 4px; -} - -.dashboard-grid::-webkit-scrollbar-thumb:hover { - background: rgba(100, 150, 200, 0.5); -} - -/* Dashboard Cards */ -.dashboard-card { - background: rgba(15, 20, 30, 0.9); - border: 1px solid rgba(100, 150, 200, 0.4); - border-left: 3px solid rgba(100, 150, 200, 0.5); - border-radius: 4px; - padding: 1.25rem; - box-shadow: - 0 0 20px rgba(100, 150, 200, 0.1), - 0 4px 16px rgba(0, 0, 0, 0.6); - transition: all 0.2s ease; -} - -.dashboard-card:hover { - border-left-color: rgba(150, 200, 255, 0.8); - box-shadow: - 0 0 25px rgba(100, 150, 200, 0.2), - 0 4px 20px rgba(0, 0, 0, 0.7); -} - -.card-wide { - grid-column: span 2; -} - -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid rgba(100, 150, 200, 0.2); -} - -.card-title { - font-size: 1rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(200, 220, 255, 1); -} - -.card-status { - padding: 0.25rem 0.75rem; - background: rgba(100, 200, 150, 0.2); - border: 1px solid rgba(100, 200, 150, 0.4); - border-radius: 3px; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(150, 255, 200, 0.9); -} - -.card-badge { - padding: 0.25rem 0.625rem; - background: rgba(100, 150, 200, 0.2); - border: 1px solid rgba(100, 150, 200, 0.4); - border-radius: 3px; - font-size: 0.75rem; - font-weight: 600; - color: rgba(150, 200, 255, 0.9); -} - -.card-content { - color: rgba(180, 200, 220, 0.9); -} - -/* Stat Grid */ -.stat-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 1.25rem; -} - -.stat-item { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 1rem; - background: rgba(20, 30, 45, 0.6); - border: 1px solid rgba(100, 150, 200, 0.3); - border-radius: 4px; -} - -.stat-label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(140, 160, 180, 0.85); -} - -.stat-value { - font-size: 1.75rem; - font-weight: 600; - color: rgba(200, 220, 255, 1); -} - -/* Member List */ -.member-list { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.member-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem; - background: rgba(20, 30, 45, 0.5); - border: 1px solid rgba(100, 150, 200, 0.2); - border-radius: 4px; - transition: all 0.15s ease; -} - -.member-item:hover { - background: rgba(30, 45, 70, 0.7); - border-color: rgba(100, 150, 200, 0.4); -} - -.member-status { - width: 10px; - height: 10px; - border-radius: 50%; - background: rgba(100, 100, 100, 0.5); -} - -.member-status.online { - background: rgba(100, 200, 150, 0.9); - box-shadow: 0 0 8px rgba(100, 200, 150, 0.5); -} - -.member-info { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.member-name { - font-size: 0.875rem; - font-weight: 500; - color: rgba(200, 220, 240, 0.95); -} - -.member-rank { - font-size: 0.75rem; - color: rgba(140, 160, 180, 0.8); -} - -/* Activity List */ -.activity-list { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.activity-item { - padding: 0.75rem; - background: rgba(20, 30, 45, 0.5); - border-left: 2px solid rgba(100, 150, 200, 0.4); - border-radius: 2px; -} - -.activity-time { - font-size: 0.7rem; - color: rgba(100, 150, 200, 0.7); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 0.375rem; -} - -.activity-text { - font-size: 0.875rem; - color: rgba(180, 200, 220, 0.9); - line-height: 1.4; -} - -/* Asset List */ -.asset-list { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.asset-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem; - background: rgba(20, 30, 45, 0.5); - border: 1px solid rgba(100, 150, 200, 0.2); - border-radius: 4px; - transition: all 0.15s ease; -} - -.asset-item:hover { - background: rgba(30, 45, 70, 0.7); - border-color: rgba(100, 150, 200, 0.4); -} - -.asset-icon { - font-size: 1.5rem; -} - -.asset-info { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.asset-name { - font-size: 0.875rem; - font-weight: 500; - color: rgba(200, 220, 240, 0.95); -} - -.asset-location { - font-size: 0.75rem; - color: rgba(140, 160, 180, 0.8); -} - -/* Mission List */ -.mission-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.mission-item { - padding: 1rem; - background: rgba(20, 30, 45, 0.6); - border: 1px solid rgba(100, 150, 200, 0.3); - border-left: 3px solid rgba(100, 150, 200, 0.5); - border-radius: 4px; -} - -.mission-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.5rem; -} - -.mission-name { - font-size: 0.95rem; - font-weight: 600; - color: rgba(200, 220, 255, 1); -} - -.mission-priority { - padding: 0.25rem 0.625rem; - border-radius: 3px; - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 600; -} - -.mission-priority.high { - background: rgba(200, 100, 100, 0.2); - border: 1px solid rgba(200, 100, 100, 0.4); - color: rgba(255, 150, 150, 0.9); -} - -.mission-priority.medium { - background: rgba(200, 150, 100, 0.2); - border: 1px solid rgba(200, 150, 100, 0.4); - color: rgba(255, 200, 150, 0.9); -} - -.mission-priority.low { - background: rgba(100, 150, 200, 0.2); - border: 1px solid rgba(100, 150, 200, 0.4); - color: rgba(150, 200, 255, 0.9); -} - -.mission-description { - font-size: 0.85rem; - color: rgba(160, 180, 200, 0.85); - margin-bottom: 0.75rem; - line-height: 1.4; -} - -.mission-progress { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.progress-bar { - flex: 1; - height: 6px; - background: rgba(20, 30, 45, 0.8); - border: 1px solid rgba(100, 150, 200, 0.3); - border-radius: 3px; - overflow: hidden; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, - rgba(100, 150, 200, 0.6), - rgba(150, 200, 255, 0.8)); - box-shadow: 0 0 10px rgba(100, 150, 200, 0.5); - transition: width 0.3s ease; -} - -.progress-text { - font-size: 0.75rem; - font-weight: 600; - color: rgba(100, 150, 200, 0.9); - min-width: 40px; -} - -/* Responsive adjustments */ -@media (max-width: 1200px) { - .card-wide { - grid-column: span 1; - } -} - -@media (max-width: 768px) { - .dashboard-container { - padding: 1rem; - } - - .dashboard-grid { - grid-template-columns: 1fr; - } -} diff --git a/arma/ui/app.js b/arma/ui/app.js deleted file mode 100644 index 02a2219..0000000 --- a/arma/ui/app.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Simple React-like Vanilla JS Implementation - */ - -// --- 1. The "Library" Logic --- - -// Helper to create DOM elements (like React.createElement) -function h(tag, props = {}, ...children) { - const el = document.createElement(tag); - - // Handle props - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith('on') && typeof value === 'function') { - el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === 'className') { - el.className = value; - } else if (key === 'style' && typeof value === 'object') { - Object.assign(el.style, value); - } else { - el.setAttribute(key, value); - } - }); - } - - // Handle children - children.forEach(child => { - if (typeof child === 'string' || typeof child === 'number') { - el.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - el.appendChild(child); - } else if (Array.isArray(child)) { - child.forEach(c => el.appendChild(c)); - } - }); - - return el; -} - -// Simple Rendering Logic -let _rootContainer = null; -let _rootComponent = null; - -function render(component, container) { - _rootContainer = container; - _rootComponent = component; - _render(); -} - -function _render() { - _rootContainer.innerHTML = ''; // Clear previous tree (simple re-render) - _rootContainer.appendChild(_rootComponent()); -} - -// Simple State Hook (Global for simplicity, or localized using closures) -// Note: In a real app this would be more complex to handle multiple components. -// For this demo, we'll re-render the whole app on state change. -const createSignal = (initialValue) => { - let _val = initialValue; - const getValue = () => _val; - const setValue = (newValue) => { - _val = typeof newValue === 'function' ? newValue(_val) : newValue; - _render(); // Trigger re-render - }; - return [getValue, setValue]; -}; - -// --- 2. The Application Components --- - -// Global View State: 'home', 'login', 'create' -const [getView, setView] = createSignal('home'); - -// Header Component -function Header({ title }) { - return h('div', { className: 'header' }, - h('h1', { - style: { cursor: 'pointer' }, - onClick: () => setView('home') - }, title), - h('p', null, 'Organization Registration & Management Portal') - ); -} - -// Login Form Component -function LoginForm() { - const handleSubmit = (e) => { - e.preventDefault(); // Critical for strict sandbox - const formData = new FormData(e.target); - const data = Object.fromEntries(formData.entries()); - console.log('Login Attempt:', data); - // TODO: Handle authentication logic here - }; - - return h('div', { className: 'card', style: { maxWidth: '400px', margin: '0 auto' } }, - h('h2', null, 'Organization Login'), - h('form', { onSubmit: handleSubmit }, - h('div', null, - h('label', null, 'Email'), - h('input', { name: 'email', type: 'text', placeholder: 'admin@spearnet.mil' }) - ), - h('div', null, - h('label', null, 'Password'), - h('input', { name: 'password', type: 'password', placeholder: '••••••••' }) - ), - h('div', { className: 'form-actions' }, - h('button', { type: 'submit', style: { width: '100%' } }, 'Access Authenticator'), - h('span', { - className: 'cancel-link', - onClick: () => setView('home') - }, 'Cancel / Return to Main') - ) - ) - ); -} - -// Create Org Form Component -function CreateOrgForm() { - const handleSubmit = (e) => { - e.preventDefault(); // Critical for strict sandbox - const formData = new FormData(e.target); - const data = Object.fromEntries(formData.entries()); - console.log('Org Registration:', data); - // TODO: Handle registration logic here - }; - - return h('div', { className: 'split-container' }, - h('div', { className: 'info-panel' }, - h('h2', null, 'Registration Details'), - h('p', null, 'Complete the form to add your organization to the Global Organization Registry.'), - h('ul', { style: { textAlign: 'left', marginTop: '1.5rem', listStyleType: 'none', padding: 0 } }, - h('li', { style: { marginBottom: '0.5rem' } }, '✅ Official Organization Designator'), - h('li', { style: { marginBottom: '0.5rem' } }, '✅ Secure Comms Channel'), - h('li', { style: { marginBottom: '0.5rem' } }, '✅ Deployment Roster Access'), - h('li', { style: { marginBottom: '0.5rem' } }, '✅ After-Action Report Tools') - ), - h('div', { className: 'price-tag', style: { marginTop: '2rem', padding: '1rem', background: 'var(--bg-app)', borderRadius: 'var(--radius)', border: '1px solid var(--border)' } }, - h('span', { style: { display: 'block', fontSize: '0.9rem', color: 'var(--text-muted)' } }, 'Registration Fee'), - h('span', { style: { display: 'block', fontSize: '2rem', fontWeight: '700', color: 'var(--primary)' } }, '$50,000') - ) - ), - h('div', { className: 'form-panel card', style: { margin: 0 } }, - h('h2', null, 'Organization Registration'), - h('form', { onSubmit: handleSubmit }, - h('div', null, - h('label', null, 'Organization Name'), - h('input', { name: 'orgName', type: 'text', placeholder: 'e.g. Task Force 141' }) - ), - h('div', null, - h('label', null, 'Organization Type'), - h('select', { name: 'type' }, - h('option', { value: 'infantry' }, 'Infantry / Milsim'), - h('option', { value: 'aviation' }, 'Aviation Wing'), - h('option', { value: 'pmc' }, 'Private Military Company'), - h('option', { value: 'support' }, 'Logistics & Support') - ) - ), - h('div', { className: 'form-actions' }, - h('button', { type: 'submit', style: { width: '100%' } }, 'Submit Registration'), - h('span', { - className: 'cancel-link', - onClick: () => setView('home') - }, 'Cancel / Return to Main') - ) - ) - ) - ); -} - -// Home View Component -function HomeView() { - return h('div', { className: 'content' }, - h('div', { className: 'card' }, - h('h2', null, 'Create Organization'), - h('p', null, 'Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.'), - h('button', { onClick: () => setView('create') }, 'Register') - ), - h('div', { className: 'card' }, - h('h2', null, 'Organization Dashboard'), - h('p', null, 'Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.'), - h('button', { onClick: () => setView('login') }, 'Login') - ) - ); -} - -// Footer Component (unchanged) -function Footer() { - return h('div', { className: 'footer' }, - h('div', { className: 'wrapper' }, - h('div', null, - h('h3', null, 'Registry Resources'), - h('ul', { style: { listStyleType: 'none', padding: 0 } }, - h('li', null, 'Registration Guidelines'), - h('li', null, 'Tax & Fee Schedule'), - h('li', null, 'Legal Compliance'), - h('li', null, 'Trademark Database') - ) - ), - h('div', null, - h('h3', null, 'Bureau Support'), - h('ul', { style: { listStyleType: 'none', padding: 0 } }, - h('li', null, 'Office: Sector 7 Admin Block'), - h('li', null, 'Hours: 0800 - 1600 (GST)'), - h('li', null, 'Helpdesk: 555-01-REGISTRY'), - h('li', null, 'support@org-bureau.gov') - ) - ) - ) - ); -} - -// Main App Component -function App() { - const view = getView(); - - let mainContent; - if (view === 'home') { - mainContent = HomeView(); - } else if (view === 'login') { - mainContent = LoginForm(); - } else if (view === 'create') { - mainContent = CreateOrgForm(); - } - - return h('main', null, - h('div', { className: 'container' }, - Header({ title: 'Global Organization Network' }), - mainContent - ), - Footer() - ); -} - -// --- 3. Mount Application --- -const root = document.getElementById('app'); -render(App, root); diff --git a/arma/ui/apps/base.css b/arma/ui/apps/base.css new file mode 100644 index 0000000..a05b977 --- /dev/null +++ b/arma/ui/apps/base.css @@ -0,0 +1,175 @@ +:root { + --bg-app: #fdfcf8; + --bg-surface: #ffffff; + --bg-surface-hover: #f1f5f9; + --primary: #475569; + --primary-hover: #1e293b; + --text-main: #1f2937; + --text-muted: #64748b; + --text-inverse: #f8fafc; + --border: #e2e8f0; + --radius: 8px; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --footer-bg: #1e293b; +} + +body { + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; + margin: 0; + padding: 0; + background: var(--bg-app); + color: var(--text-main); + line-height: 1.6; +} + +#app { + min-height: 100vh; +} + +main { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.container { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 2rem; + flex: 1; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.header { + text-align: center; + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border); + + h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + letter-spacing: -0.025em; + color: var(--primary-hover); + } + + p { + color: var(--text-muted); + font-size: 1.1rem; + } +} + +.card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; + box-shadow: var(--shadow); + text-align: center; + + h2 { + margin-top: 0; + font-size: 1.8rem; + color: var(--primary-hover); + } +} + +button { + background: var(--primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--radius); + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.65; + transform: none; + box-shadow: none; + } + + & + & { + margin-left: 1rem; + } +} + +.footer { + margin-top: auto; + background: var(--footer-bg); + color: var(--text-inverse); + display: block; + + .wrapper { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 3rem 2rem; + box-sizing: border-box; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + } + + h3 { + color: var(--text-inverse); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 700; + margin-bottom: 1.5rem; + border-bottom: 1px solid #475569; + padding-bottom: 0.5rem; + margin-right: 1rem; + } + + ul { + li { + color: #cbd5e1; + font-size: 0.95rem; + margin-bottom: 0.75rem; + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: white; + } + } + } +} + +@media (max-width: 960px) { + .container { + padding: 1.5rem; + } + + .header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + + h1 { + font-size: 2rem; + } + } + + .footer .wrapper { + grid-template-columns: 1fr; + } +} diff --git a/arma/ui/apps/main/bootstrap.js b/arma/ui/apps/main/bootstrap.js new file mode 100644 index 0000000..663a0b3 --- /dev/null +++ b/arma/ui/apps/main/bootstrap.js @@ -0,0 +1,6 @@ +/** + * Registry app bootstrap + */ + +const root = document.getElementById("app"); +window.RegistryApp.runtime.render(window.RegistryApp.components.App, root); diff --git a/arma/ui/apps/main/components/createOrgForm.js b/arma/ui/apps/main/components/createOrgForm.js new file mode 100644 index 0000000..2541103 --- /dev/null +++ b/arma/ui/apps/main/components/createOrgForm.js @@ -0,0 +1,168 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() { + const handleCreate = () => { + const data = { + orgName: String( + document.getElementById("org-create-name")?.value || "", + ), + type: String( + document.getElementById("org-create-type")?.value || "", + ), + }; + console.log("Org Registration:", data); + }; + + return h( + "div", + { className: "split-container" }, + h( + "div", + { className: "info-panel" }, + h("h2", null, "Registration Details"), + h( + "p", + null, + "Complete the form to add your organization to the Global Organization Registry.", + ), + h( + "ul", + { + style: { + textAlign: "left", + marginTop: "1.5rem", + listStyleType: "none", + padding: 0, + }, + }, + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Official Organization Designator", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Secure Comms Channel", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ Deployment Roster Access", + ), + h( + "li", + { style: { marginBottom: "0.5rem" } }, + "✅ After-Action Report Tools", + ), + ), + h( + "div", + { + className: "price-tag", + style: { + marginTop: "2rem", + padding: "1rem", + background: "var(--bg-app)", + borderRadius: "var(--radius)", + border: "1px solid var(--border)", + }, + }, + h( + "span", + { + style: { + display: "block", + fontSize: "0.9rem", + color: "var(--text-muted)", + }, + }, + "Registration Fee", + ), + h( + "span", + { + style: { + display: "block", + fontSize: "2rem", + fontWeight: "700", + color: "var(--primary)", + }, + }, + "$50,000", + ), + ), + ), + h( + "div", + { className: "form-panel card", style: { margin: 0 } }, + h("h2", null, "Organization Registration"), + h( + "div", + { className: "app-form" }, + h( + "div", + null, + h("label", null, "Organization Name"), + h("input", { + id: "org-create-name", + type: "text", + placeholder: "e.g. Task Force 141", + }), + ), + h( + "div", + null, + h("label", null, "Organization Type"), + h( + "select", + { id: "org-create-type" }, + h( + "option", + { value: "infantry" }, + "Infantry / Milsim", + ), + h("option", { value: "aviation" }, "Aviation Wing"), + h( + "option", + { value: "pmc" }, + "Private Military Company", + ), + h( + "option", + { value: "support" }, + "Logistics & Support", + ), + ), + ), + h( + "div", + { className: "form-actions" }, + h( + "button", + { + type: "button", + style: { width: "100%" }, + onClick: handleCreate, + }, + "Submit Registration", + ), + h( + "span", + { + className: "cancel-link", + onClick: () => store.setView("home"), + }, + "Cancel / Return to Main", + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/footer.js b/arma/ui/apps/main/components/footer.js new file mode 100644 index 0000000..76f53c4 --- /dev/null +++ b/arma/ui/apps/main/components/footer.js @@ -0,0 +1,43 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.Footer = function Footer() { + return h( + "div", + { className: "footer" }, + h( + "div", + { className: "wrapper" }, + h( + "div", + null, + h("h3", null, "Registry Resources"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Registration Guidelines"), + h("li", null, "Tax & Fee Schedule"), + h("li", null, "Legal Compliance"), + h("li", null, "Trademark Database"), + ), + ), + h( + "div", + null, + h("h3", null, "Bureau Support"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Office: Sector 7 Admin Block"), + h("li", null, "Hours: 0800 - 1600 (GST)"), + h("li", null, "Helpdesk: 555-01-REGISTRY"), + h("li", null, "support@org-bureau.gov"), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/forms.css b/arma/ui/apps/main/components/forms.css new file mode 100644 index 0000000..8364d77 --- /dev/null +++ b/arma/ui/apps/main/components/forms.css @@ -0,0 +1,72 @@ +.split-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: center; + width: 100%; +} + +.info-panel { + text-align: left; + padding: 1rem; +} + +.app-form { + display: flex; + flex-direction: column; + gap: 1rem; + text-align: left; + + label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; + } + + input, + select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); + } + } +} + +.form-actions { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.cancel-link { + font-size: 0.9rem; + color: var(--text-muted); + cursor: pointer; + text-decoration: underline; + + &:hover { + color: var(--primary); + } +} + +@media (max-width: 960px) { + .split-container { + grid-template-columns: 1fr; + } +} diff --git a/arma/ui/apps/main/components/header.js b/arma/ui/apps/main/components/header.js new file mode 100644 index 0000000..bfb199c --- /dev/null +++ b/arma/ui/apps/main/components/header.js @@ -0,0 +1,23 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.Header = function Header({ title }) { + return h( + "div", + { className: "header" }, + h( + "h1", + { + style: { cursor: "pointer" }, + onClick: () => store.setView("home"), + }, + title, + ), + h("p", null, "Organization Registration & Management Portal"), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/homeView.css b/arma/ui/apps/main/components/homeView.css new file mode 100644 index 0000000..fb1fd66 --- /dev/null +++ b/arma/ui/apps/main/components/homeView.css @@ -0,0 +1,12 @@ +.content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 960px) { + .content { + grid-template-columns: 1fr; + } +} diff --git a/arma/ui/apps/main/components/homeView.js b/arma/ui/apps/main/components/homeView.js new file mode 100644 index 0000000..5342229 --- /dev/null +++ b/arma/ui/apps/main/components/homeView.js @@ -0,0 +1,57 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.HomeView = function HomeView() { + return h( + "div", + { className: "content" }, + h( + "div", + { className: "card" }, + h("h2", null, "Create Organization"), + h( + "p", + null, + "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", + ), + h( + "button", + { onClick: () => store.setView("create") }, + "Register", + ), + ), + h( + "div", + { className: "card" }, + h("h2", null, "Organization Portal"), + h( + "p", + null, + "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", + ), + h("button", { onClick: () => store.setView("login") }, "Login"), + ), + h( + "div", + { className: "card", style: { gridColumn: "span 2" } }, + h("h2", null, "Organization Portal Preview"), + h( + "p", + null, + "Review the refactor direction for a player organization portal with fleet, assets, treasury, reputation, roster management, and reserved space for future modules.", + ), + h( + "button", + { + onClick: () => store.setView("portal"), + }, + "Open Portal Preview", + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/index.js b/arma/ui/apps/main/components/index.js new file mode 100644 index 0000000..8b63cea --- /dev/null +++ b/arma/ui/apps/main/components/index.js @@ -0,0 +1,48 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.components = RegistryApp.components || {}; + + RegistryApp.components.App = function App() { + const Navbar = RegistryApp.componentFns.Navbar; + const Header = RegistryApp.componentFns.Header; + const HomeView = RegistryApp.componentFns.HomeView; + const LoginForm = RegistryApp.componentFns.LoginForm; + const CreateOrgForm = RegistryApp.componentFns.CreateOrgForm; + const Footer = RegistryApp.componentFns.Footer; + const PortalApp = + window.OrgPortal && window.OrgPortal.components + ? window.OrgPortal.components.App + : null; + + const view = store.getView(); + + if (view === "portal" && PortalApp) { + return h("div", null, Navbar(), PortalApp()); + } + + let mainContent; + if (view === "home") { + mainContent = HomeView(); + } else if (view === "login") { + mainContent = LoginForm(); + } else if (view === "create") { + mainContent = CreateOrgForm(); + } + + return h( + "main", + null, + Navbar(), + h( + "div", + { className: "container" }, + Header({ title: "Global Organization Network" }), + mainContent, + ), + Footer(), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/loginForm.js b/arma/ui/apps/main/components/loginForm.js new file mode 100644 index 0000000..82e87a3 --- /dev/null +++ b/arma/ui/apps/main/components/loginForm.js @@ -0,0 +1,76 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.LoginForm = function LoginForm() { + const handleLogin = () => { + const data = { + email: String( + document.getElementById("org-login-email")?.value || "", + ), + password: String( + document.getElementById("org-login-password")?.value || "", + ), + }; + console.log("Login Attempt:", data); + store.setView("portal"); + }; + + return h( + "div", + { + className: "card", + style: { maxWidth: "400px", margin: "0 auto" }, + }, + h("h2", null, "Organization Login"), + h( + "div", + { className: "app-form" }, + h( + "div", + null, + h("label", null, "Email"), + h("input", { + id: "org-login-email", + type: "text", + placeholder: "admin@spearnet.mil", + }), + ), + h( + "div", + null, + h("label", null, "Password"), + h("input", { + id: "org-login-password", + type: "password", + placeholder: "********", + }), + ), + h( + "div", + { className: "form-actions" }, + h( + "button", + { + type: "button", + style: { width: "100%" }, + onClick: handleLogin, + }, + "Access Authenticator", + ), + h( + "span", + { + className: "cancel-link", + onClick: () => store.setView("home"), + }, + "Cancel / Return to Main", + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/navbar.css b/arma/ui/apps/main/components/navbar.css new file mode 100644 index 0000000..9a6478e --- /dev/null +++ b/arma/ui/apps/main/components/navbar.css @@ -0,0 +1,79 @@ +.app-navbar { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.app-navbar-inner { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 1rem 2rem; + box-sizing: border-box; +} + +.app-navbar-brand { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.app-navbar-kicker { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; +} + +.app-navbar-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--primary-hover); + letter-spacing: -0.025em; +} + +.app-navbar-actions { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.app-navbar-view { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + font-weight: 600; +} + +.app-close-btn { + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border); + padding: 0.5rem 1rem; + font-size: 0.85rem; + + &:hover { + background: var(--bg-surface-hover); + color: var(--primary-hover); + border-color: var(--primary); + transform: none; + box-shadow: none; + } +} + +@media (max-width: 960px) { + .app-navbar-inner { + flex-direction: column; + align-items: flex-start; + padding: 1rem 1.5rem; + } + + .app-navbar-actions { + align-items: flex-start; + } +} diff --git a/arma/ui/apps/main/components/navbar.js b/arma/ui/apps/main/components/navbar.js new file mode 100644 index 0000000..53a7b5a --- /dev/null +++ b/arma/ui/apps/main/components/navbar.js @@ -0,0 +1,70 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + function closeRegistry() { + if ( + typeof A3API !== "undefined" && + typeof A3API.SendAlert === "function" + ) { + A3API.SendAlert( + JSON.stringify({ + event: "org::close", + data: {}, + }), + ); + return; + } + + store.setView("home"); + } + + RegistryApp.componentFns.Navbar = function Navbar() { + const view = store.getView(); + const viewLabel = + view === "login" + ? "Organization Login" + : view === "create" + ? "Organization Registration" + : view === "portal" + ? "Organization Portal" + : "Entry Hub"; + const actionLabel = view === "portal" ? "Sign Out" : "Close"; + + return h( + "nav", + { className: "app-navbar" }, + h( + "div", + { className: "app-navbar-inner" }, + h( + "div", + { className: "app-navbar-brand" }, + h("span", { className: "app-navbar-kicker" }, "ORBIS"), + h( + "span", + { className: "app-navbar-title" }, + "Global Organization Network", + ), + ), + h( + "div", + { className: "app-navbar-actions" }, + h("span", { className: "app-navbar-view" }, viewLabel), + h( + "button", + { + type: "button", + className: "app-close-btn", + onClick: closeRegistry, + }, + actionLabel, + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/main/index.html b/arma/ui/apps/main/index.html new file mode 100644 index 0000000..aeb45f8 --- /dev/null +++ b/arma/ui/apps/main/index.html @@ -0,0 +1,58 @@ + + + + + + ORBIS - Global Organization Network + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/arma/ui/apps/main/runtime.js b/arma/ui/apps/main/runtime.js new file mode 100644 index 0000000..70f267b --- /dev/null +++ b/arma/ui/apps/main/runtime.js @@ -0,0 +1,73 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + + function h(tag, props = {}, ...children) { + const el = document.createElement(tag); + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith("on") && typeof value === "function") { + el.addEventListener(key.substring(2).toLowerCase(), value); + } else if (key === "className") { + el.className = value; + } else if (key === "style" && typeof value === "object") { + Object.assign(el.style, value); + } else if (typeof value === "boolean") { + if (value) { + el.setAttribute(key, ""); + } else { + el.removeAttribute(key); + } + } else if (value === null || value === undefined) { + el.removeAttribute(key); + } else { + el.setAttribute(key, value); + } + }); + } + + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { + el.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + el.appendChild(child); + } else if (Array.isArray(child)) { + child.forEach((c) => el.appendChild(c)); + } + }); + + return el; + } + + let rootContainer = null; + let rootComponent = null; + + function render(component, container) { + rootContainer = container; + rootComponent = component; + rerender(); + } + + function rerender() { + rootContainer.innerHTML = ""; + rootContainer.appendChild(rootComponent()); + } + + function createSignal(initialValue) { + let value = initialValue; + + const getValue = () => value; + const setValue = (newValue) => { + value = typeof newValue === "function" ? newValue(value) : newValue; + rerender(); + }; + + return [getValue, setValue]; + } + + RegistryApp.runtime = { + h, + render, + createSignal, + }; +})(); diff --git a/arma/ui/apps/main/state.js b/arma/ui/apps/main/state.js new file mode 100644 index 0000000..bb52993 --- /dev/null +++ b/arma/ui/apps/main/state.js @@ -0,0 +1,12 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { createSignal } = RegistryApp.runtime; + + class RegistryStore { + constructor() { + [this.getView, this.setView] = createSignal("home"); + } + } + + RegistryApp.store = new RegistryStore(); +})(); diff --git a/arma/ui/apps/portal/actions.js b/arma/ui/apps/portal/actions.js new file mode 100644 index 0000000..ac813cd --- /dev/null +++ b/arma/ui/apps/portal/actions.js @@ -0,0 +1,267 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const registryStore = window.RegistryApp.store; + + class OrgPortalActions { + constructor() { + this.treasuryNoticeTimer = null; + } + + formatCurrency(value) { + return "$" + value.toLocaleString(); + } + + formatVehicleType(type) { + if (!type) { + return ""; + } + + return type.charAt(0).toUpperCase() + type.slice(1); + } + + formatAssetType(type) { + if (!type) { + return ""; + } + + return type.charAt(0).toUpperCase() + type.slice(1); + } + + getAssetReadiness() { + const total = portalData.fleet.reduce( + (sum, unit) => sum + (100 - parseInt(unit.damage, 10)), + 0, + ); + return Math.round(total / portalData.fleet.length); + } + + showTreasuryNotice(type, text) { + store.setTreasuryNotice({ type, text }); + + if (this.treasuryNoticeTimer) { + clearTimeout(this.treasuryNoticeTimer); + } + + this.treasuryNoticeTimer = setTimeout(() => { + store.setTreasuryNotice({ type: "", text: "" }); + this.treasuryNoticeTimer = null; + }, 3500); + } + + parseAmount(value) { + const amount = Number(value); + return Number.isFinite(amount) ? Math.round(amount) : 0; + } + + getInputValue(id) { + const el = document.getElementById(id); + return el ? el.value : ""; + } + + closePortal() { + if ( + typeof A3API !== "undefined" && + typeof A3API.SendAlert === "function" + ) { + A3API.SendAlert( + JSON.stringify({ + event: "org::close", + data: {}, + }), + ); + return; + } + + if (registryStore) { + registryStore.setView("home"); + } + } + + openModal(type) { + if ( + (type === "payroll" || + type === "transfer" || + type === "credit") && + !permissions.canManageTreasury() + ) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return; + } + + if (type === "disband" && !permissions.canDisbandOrg()) { + return; + } + + store.setModal({ type }); + } + + closeModal() { + store.setModal(null); + } + + removeMember(memberName) { + if (!permissions.canManageMembers()) { + return false; + } + + store.setMembers((currentMembers) => + currentMembers.filter((member) => member.name !== memberName), + ); + store.setCreditLines((currentLines) => + currentLines.filter((line) => line.member !== memberName), + ); + return true; + } + + disbandOrganization() { + if (!permissions.canDisbandOrg()) { + return false; + } + + store.setOrgDisbanded(true); + this.closeModal(); + return true; + } + + runPayroll(amountPerMember) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + const members = store.getMembers(); + const funds = store.getFunds(); + + if (members.length === 0) { + this.showTreasuryNotice( + "error", + "No members available for payroll.", + ); + return false; + } + + if (amountPerMember <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid payroll amount.", + ); + return false; + } + + const total = amountPerMember * members.length; + if (total > funds) { + this.showTreasuryNotice( + "error", + "Insufficient org funds for payroll.", + ); + return false; + } + + store.setFunds(funds - total); + this.showTreasuryNotice( + "success", + `Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`, + ); + return true; + } + + sendFundsToMember(memberName, amount) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + const funds = store.getFunds(); + + if (!memberName) { + this.showTreasuryNotice( + "error", + "Select a member to receive funds.", + ); + return false; + } + + if (amount <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid transfer amount.", + ); + return false; + } + + if (amount > funds) { + this.showTreasuryNotice( + "error", + "Insufficient org funds for this transfer.", + ); + return false; + } + + store.setFunds(funds - amount); + this.showTreasuryNotice( + "success", + `${this.formatCurrency(amount)} sent to ${memberName}.`, + ); + return true; + } + + grantCreditLine(memberName, amount) { + if (!permissions.canManageTreasury()) { + this.showTreasuryNotice( + "error", + "Only the organization leader or CEO can manage treasury actions.", + ); + return false; + } + + if (!memberName) { + this.showTreasuryNotice( + "error", + "Select a member for the credit line.", + ); + return false; + } + + if (amount <= 0) { + this.showTreasuryNotice( + "error", + "Enter a valid credit line amount.", + ); + return false; + } + + store.setCreditLines((currentLines) => { + const existingIndex = currentLines.findIndex( + (line) => line.member === memberName, + ); + if (existingIndex === -1) { + return [...currentLines, { member: memberName, amount }]; + } + + const updatedLines = [...currentLines]; + updatedLines[existingIndex] = { member: memberName, amount }; + return updatedLines; + }); + + this.showTreasuryNotice( + "success", + `Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`, + ); + return true; + } + } + + OrgPortal.actions = new OrgPortalActions(); +})(); diff --git a/arma/ui/apps/portal/components/activityCard.css b/arma/ui/apps/portal/components/activityCard.css new file mode 100644 index 0000000..d26dbf9 --- /dev/null +++ b/arma/ui/apps/portal/components/activityCard.css @@ -0,0 +1,32 @@ +.org-activity-row { + padding: 1rem; + border: 1px solid var(--border); + border-left: 3px solid #94a3b8; + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + border-left-color: #64748b; + } + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-activity-time { + display: inline-block; + margin-bottom: 0.35rem; + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} diff --git a/arma/ui/apps/portal/components/activityCard.js b/arma/ui/apps/portal/components/activityCard.js new file mode 100644 index 0000000..0de0ea8 --- /dev/null +++ b/arma/ui/apps/portal/components/activityCard.js @@ -0,0 +1,44 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.ActivityCard = function ActivityCard() { + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-6" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Command Feed"), + h( + "p", + { className: "org-panel-subtitle" }, + "Recent organization-level actions and updates.", + ), + ), + ), + h( + "div", + { className: "org-activity-list" }, + ...portalData.activity.map((item) => + h( + "article", + { className: "org-activity-row" }, + h( + "span", + { className: "org-activity-time" }, + item.time, + ), + h("p", null, item.text), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/assetsCard.js b/arma/ui/apps/portal/components/assetsCard.js new file mode 100644 index 0000000..86473fc --- /dev/null +++ b/arma/ui/apps/portal/components/assetsCard.js @@ -0,0 +1,55 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.AssetsCard = function AssetsCard() { + const SimpleStat = OrgPortal.componentFns.SimpleStat; + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-7" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Assets"), + h( + "p", + { className: "org-panel-subtitle" }, + "Inventory supplies and equipment with quantity totals.", + ), + ), + ), + h( + "div", + { className: "org-simple-list" }, + ...portalData.assets.map((asset) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + asset.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatAssetType(asset.type), + ), + SimpleStat("Quantity", asset.quantity), + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/controls.css b/arma/ui/apps/portal/components/controls.css new file mode 100644 index 0000000..5ed9592 --- /dev/null +++ b/arma/ui/apps/portal/components/controls.css @@ -0,0 +1,33 @@ +.org-secondary-btn { + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border); + + &:hover { + background: var(--bg-surface-hover); + color: var(--text-main); + } +} + +.org-danger-btn { + background: #7f1d1d; + color: #fef2f2; + + &:hover { + background: #991b1b; + } +} + +.org-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + padding: 0; +} + +.org-icon { + width: 1rem; + height: 1rem; +} diff --git a/arma/ui/apps/portal/components/dangerCard.css b/arma/ui/apps/portal/components/dangerCard.css new file mode 100644 index 0000000..82a8d12 --- /dev/null +++ b/arma/ui/apps/portal/components/dangerCard.css @@ -0,0 +1,22 @@ +.org-danger-panel { + border-color: #fecaca; + background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); +} + +.org-danger-copy { + margin-bottom: 1rem; + + strong, + p { + display: block; + } + + p { + margin: 0.4rem 0 0; + color: var(--text-muted); + } +} + +.org-empty-state { + text-align: left; +} diff --git a/arma/ui/apps/portal/components/dangerCard.js b/arma/ui/apps/portal/components/dangerCard.js new file mode 100644 index 0000000..16fbf31 --- /dev/null +++ b/arma/ui/apps/portal/components/dangerCard.js @@ -0,0 +1,56 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DangerCard = function DangerCard() { + if (!permissions.canDisbandOrg()) { + return null; + } + + return h( + "section", + { className: "card org-panel org-span-12 org-danger-panel" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "h2", + { className: "org-panel-title" }, + "Organization Controls", + ), + h( + "p", + { className: "org-panel-subtitle" }, + "Leader-only actions for membership and permanent organization removal.", + ), + ), + ), + h( + "div", + { className: "org-danger-copy" }, + h("strong", null, "Disband organization"), + h( + "p", + null, + "This removes the organization and revokes access to the portal for all members.", + ), + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.openModal("disband"), + }, + "Disband Organization", + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/disbandedView.js b/arma/ui/apps/portal/components/disbandedView.js new file mode 100644 index 0000000..94d4497 --- /dev/null +++ b/arma/ui/apps/portal/components/disbandedView.js @@ -0,0 +1,47 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const registryStore = window.RegistryApp.store; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DisbandedView = function DisbandedView() { + return h( + "section", + { className: "card org-panel org-span-12 org-empty-state" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "div", + { className: "org-eyebrow" }, + "Organization Removed", + ), + h( + "h2", + { className: "org-panel-title" }, + portalData.org.name, + ), + ), + ), + h( + "p", + { className: "org-summary" }, + "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => registryStore.setView("home"), + }, + "Return to Registry", + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/fleetCard.js b/arma/ui/apps/portal/components/fleetCard.js new file mode 100644 index 0000000..7e69f37 --- /dev/null +++ b/arma/ui/apps/portal/components/fleetCard.js @@ -0,0 +1,56 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FleetCard = function FleetCard() { + const SimpleStat = OrgPortal.componentFns.SimpleStat; + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-7" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Fleet"), + h( + "p", + { className: "org-panel-subtitle" }, + "Individual vehicles with type, status, and overall damage.", + ), + ), + ), + h( + "div", + { className: "org-simple-list" }, + ...portalData.fleet.map((unit) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + unit.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatVehicleType(unit.type), + ), + SimpleStat("Status", unit.status), + SimpleStat("Damage", unit.damage), + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/footer.js b/arma/ui/apps/portal/components/footer.js new file mode 100644 index 0000000..50454df --- /dev/null +++ b/arma/ui/apps/portal/components/footer.js @@ -0,0 +1,43 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.Footer = function Footer() { + return h( + "div", + { className: "footer" }, + h( + "div", + { className: "wrapper" }, + h( + "div", + null, + h("h3", null, "Organization Controls"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Roster Management"), + h("li", null, "Fleet Assignment"), + h("li", null, "Treasury Permissions"), + h("li", null, "Asset Registry"), + ), + ), + h( + "div", + null, + h("h3", null, "Planned Extensions"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Contracts Board"), + h("li", null, "Diplomacy Layer"), + h("li", null, "Procurement Queue"), + h("li", null, "Reputation History"), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/futureCard.css b/arma/ui/apps/portal/components/futureCard.css new file mode 100644 index 0000000..ad6407e --- /dev/null +++ b/arma/ui/apps/portal/components/futureCard.css @@ -0,0 +1,74 @@ +.org-roadmap-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + flex: 1; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; +} + +.org-roadmap-card { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.7rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(4n + 2), + &:nth-child(4n + 3) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(100 116 139 / 0.4); + } + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-list-tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + background: #e2e8f0; + color: var(--primary-hover); + + .org-roadmap-card:nth-child(4n + 2) &, + .org-roadmap-card:nth-child(4n + 3) & { + background: #cbd5e1; + color: #1e293b; + } +} + +@media (max-width: 960px) { + .org-roadmap-grid { + grid-template-columns: 1fr; + } + + .org-roadmap-card { + &:nth-child(4n + 3) { + background: #f8fafc; + border-color: var(--border); + } + } + + .org-list-tag { + .org-roadmap-card:nth-child(4n + 3) & { + background: #e2e8f0; + color: var(--primary-hover); + } + } +} diff --git a/arma/ui/apps/portal/components/futureCard.js b/arma/ui/apps/portal/components/futureCard.js new file mode 100644 index 0000000..72b7158 --- /dev/null +++ b/arma/ui/apps/portal/components/futureCard.js @@ -0,0 +1,45 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FutureCard = function FutureCard() { + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-6" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h( + "h2", + { className: "org-panel-title" }, + "Expansion Slots", + ), + h( + "p", + { className: "org-panel-subtitle" }, + "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", + ), + ), + ), + h( + "div", + { className: "org-roadmap-grid" }, + ...portalData.roadmap.map((item) => + h( + "article", + { className: "org-roadmap-card" }, + h("span", { className: "org-list-tag" }, item.status), + h("strong", null, item.name), + h("p", null, item.detail), + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/index.js b/arma/ui/apps/portal/components/index.js new file mode 100644 index 0000000..d6a80e9 --- /dev/null +++ b/arma/ui/apps/portal/components/index.js @@ -0,0 +1,65 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const store = OrgPortal.store; + + OrgPortal.components = OrgPortal.components || {}; + + OrgPortal.components.App = function App() { + const PortalHeader = OrgPortal.componentFns.PortalHeader; + const OverviewCard = OrgPortal.componentFns.OverviewCard; + const FleetCard = OrgPortal.componentFns.FleetCard; + const TreasuryCard = OrgPortal.componentFns.TreasuryCard; + const MembersCard = OrgPortal.componentFns.MembersCard; + const AssetsCard = OrgPortal.componentFns.AssetsCard; + const ActivityCard = OrgPortal.componentFns.ActivityCard; + const FutureCard = OrgPortal.componentFns.FutureCard; + const DangerCard = OrgPortal.componentFns.DangerCard; + const ModalLayer = OrgPortal.componentFns.ModalLayer; + const DisbandedView = OrgPortal.componentFns.DisbandedView; + const Footer = OrgPortal.componentFns.Footer; + + if (store.getOrgDisbanded()) { + return h( + "main", + null, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + PortalHeader(), + DisbandedView(), + ), + ), + ModalLayer(), + Footer(), + ); + } + + return h( + "main", + null, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + PortalHeader(), + OverviewCard(), + FleetCard(), + TreasuryCard(), + MembersCard(), + AssetsCard(), + ActivityCard(), + FutureCard(), + DangerCard(), + ), + ), + ModalLayer(), + Footer(), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/layout.css b/arma/ui/apps/portal/components/layout.css new file mode 100644 index 0000000..b46b675 --- /dev/null +++ b/arma/ui/apps/portal/components/layout.css @@ -0,0 +1,81 @@ +.org-dashboard-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 1.5rem; +} + +.org-panel { + margin-bottom: 0; + text-align: left; +} + +.org-scroll-panel { + display: flex; + flex-direction: column; + max-height: 31rem; + overflow: hidden; +} + +.org-span-12 { + grid-column: span 12; +} + +.org-span-7 { + grid-column: span 7; +} + +.org-span-6 { + grid-column: span 6; +} + +.org-span-5 { + grid-column: span 5; +} + +.org-panel-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; + + &.org-panel-head-stack { + flex-direction: column; + align-items: stretch; + } +} + +.org-eyebrow { + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.4rem; +} + +.org-panel-title { + margin: 0; + color: var(--primary-hover); + font-size: 1.45rem; +} + +.org-panel-subtitle { + margin: 0.35rem 0 0; + color: var(--text-muted); + font-size: 0.95rem; +} + +@media (max-width: 960px) { + .org-span-12, + .org-span-7, + .org-span-6, + .org-span-5 { + grid-column: span 12; + } + + .org-panel-head { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/ui/apps/portal/components/membersCard.js b/arma/ui/apps/portal/components/membersCard.js new file mode 100644 index 0000000..8333b7c --- /dev/null +++ b/arma/ui/apps/portal/components/membersCard.js @@ -0,0 +1,75 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.MembersCard = function MembersCard() { + const members = store.getMembers(); + const allowMemberManagement = permissions.canManageMembers(); + + return h( + "section", + { className: "card org-panel org-scroll-panel org-span-5" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Members"), + h( + "p", + { className: "org-panel-subtitle" }, + "Current roster listing with member removal controls.", + ), + ), + ), + h( + "div", + { className: "org-name-list" }, + ...members.map((member) => + h( + "article", + { className: "org-name-row" }, + h("strong", null, member.name), + allowMemberManagement + ? h( + "button", + { + type: "button", + className: "org-danger-btn org-icon-btn", + title: `Remove ${member.name}`, + "aria-label": `Remove ${member.name}`, + onClick: () => + actions.removeMember(member.name), + }, + h( + "svg", + { + className: "org-icon", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "aria-hidden": "true", + }, + h("path", { d: "M9 3h6" }), + h("path", { d: "M4 7h16" }), + h("path", { d: "M6 7l1 13h10l1-13" }), + h("path", { d: "M10 11v6" }), + h("path", { d: "M14 11v6" }), + ), + ) + : null, + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/metricCard.css b/arma/ui/apps/portal/components/metricCard.css new file mode 100644 index 0000000..b7d283f --- /dev/null +++ b/arma/ui/apps/portal/components/metricCard.css @@ -0,0 +1,69 @@ +.org-metric-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.org-metric-card { + display: flex; + flex-direction: column; + gap: 0.45rem; + padding: 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + + &:nth-child(4n + 2), + &:nth-child(4n + 3) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(226 232 240) 100% + ); + border-color: rgb(100 116 139 / 0.35); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); + } +} + +.org-metric-label { + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.org-metric-value { + font-size: 1.8rem; + color: var(--primary-hover); + line-height: 1.1; + + .org-metric-card:nth-child(4n + 2) &, + .org-metric-card:nth-child(4n + 3) & { + color: #334155; + } +} + +.org-metric-note { + color: var(--text-muted); + font-size: 0.9rem; +} + +@media (max-width: 960px) { + .org-metric-grid { + grid-template-columns: 1fr; + } + + .org-metric-card { + &:nth-child(4n + 3) { + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + border-color: var(--border); + box-shadow: none; + } + } + + .org-metric-value { + .org-metric-card:nth-child(4n + 3) & { + color: var(--primary-hover); + } + } +} diff --git a/arma/ui/apps/portal/components/metricCard.js b/arma/ui/apps/portal/components/metricCard.js new file mode 100644 index 0000000..065fe94 --- /dev/null +++ b/arma/ui/apps/portal/components/metricCard.js @@ -0,0 +1,20 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.MetricCard = function MetricCard( + label, + value, + note, + ) { + return h( + "div", + { className: "org-metric-card" }, + h("span", { className: "org-metric-label" }, label), + h("strong", { className: "org-metric-value" }, value), + h("span", { className: "org-metric-note" }, note), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/modalLayer.css b/arma/ui/apps/portal/components/modalLayer.css new file mode 100644 index 0000000..1b2a41d --- /dev/null +++ b/arma/ui/apps/portal/components/modalLayer.css @@ -0,0 +1,131 @@ +.org-modal-backdrop { + position: fixed; + inset: 0; + background: rgb(15 23 42 / 0.38); + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + z-index: 20; +} + +.org-modal-card { + width: min(100%, 30rem); + margin-bottom: 0; + text-align: left; +} + +.org-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.org-modal-close { + width: 2.25rem; + height: 2.25rem; + padding: 0; + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border); + box-shadow: none; + transform: none; + + &:hover { + background: var(--bg-surface-hover); + color: var(--text-main); + box-shadow: none; + transform: none; + } +} + +.org-modal-form { + display: flex; + flex-direction: column; + gap: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; + } + + input, + select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: + border-color 0.2s, + box-shadow 0.2s; + + &:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); + } + + &:disabled { + background: #f1f5f9; + color: var(--text-muted); + cursor: not-allowed; + } + } +} + +.org-modal-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; + + button + button { + margin-left: 0; + } +} + +.org-danger-confirm { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid #fecaca; + border-radius: var(--radius); + background: #fff1f2; + align-items: flex-start; + + p { + margin: 0; + color: var(--text-main); + } +} + +.org-danger-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + + button + button { + margin-left: 0; + } +} + +@media (max-width: 960px) { + .org-modal-head, + .org-danger-confirm { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/ui/apps/portal/components/modalLayer.js b/arma/ui/apps/portal/components/modalLayer.js new file mode 100644 index 0000000..af566ad --- /dev/null +++ b/arma/ui/apps/portal/components/modalLayer.js @@ -0,0 +1,286 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.ModalLayer = function ModalLayer() { + const modal = store.getModal(); + if (!modal) { + return null; + } + + const members = store.getMembers(); + const memberSelectProps = + members.length === 0 ? { disabled: true } : {}; + + let title = ""; + let body = null; + + if (modal.type === "payroll") { + title = "Run Payroll"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Amount Per Member"), + h("input", { + id: "treasury-payroll-amount", + type: "number", + min: "1", + placeholder: "500", + autofocus: "true", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + onClick: () => { + if ( + actions.runPayroll( + actions.parseAmount( + actions.getInputValue( + "treasury-payroll-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Run Payroll", + ), + ), + ); + } else if (modal.type === "transfer") { + title = "Send Funds"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Member"), + h( + "select", + { + id: "treasury-transfer-member", + ...memberSelectProps, + }, + ...members.map((member) => + h("option", { value: member.name }, member.name), + ), + ), + ), + h( + "div", + null, + h("label", null, "Amount"), + h("input", { + id: "treasury-transfer-amount", + type: "number", + min: "1", + placeholder: "1500", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + ...memberSelectProps, + onClick: () => { + if ( + actions.sendFundsToMember( + String( + actions.getInputValue( + "treasury-transfer-member", + ) || "", + ), + actions.parseAmount( + actions.getInputValue( + "treasury-transfer-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Send Funds", + ), + ), + ); + } else if (modal.type === "credit") { + title = "Assign Credit Line"; + body = h( + "div", + { className: "org-modal-form" }, + h( + "div", + null, + h("label", null, "Member"), + h( + "select", + { id: "treasury-credit-member", ...memberSelectProps }, + ...members.map((member) => + h("option", { value: member.name }, member.name), + ), + ), + ), + h( + "div", + null, + h("label", null, "Credit Amount"), + h("input", { + id: "treasury-credit-amount", + type: "number", + min: "1", + placeholder: "5000", + }), + ), + h( + "div", + { className: "org-modal-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + ...memberSelectProps, + onClick: () => { + if ( + actions.grantCreditLine( + String( + actions.getInputValue( + "treasury-credit-member", + ) || "", + ), + actions.parseAmount( + actions.getInputValue( + "treasury-credit-amount", + ), + ), + ) + ) { + actions.closeModal(); + } + }, + }, + "Assign Credit Line", + ), + ), + ); + } else if (modal.type === "disband") { + title = "Disband Organization"; + body = h( + "div", + { className: "org-danger-confirm" }, + h( + "p", + null, + "This action is permanent. Disband ", + portalData.org.name, + "?", + ), + h( + "div", + { className: "org-danger-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.disbandOrganization(), + }, + "Confirm Disband", + ), + ), + ); + } + + return h( + "div", + { + className: "org-modal-backdrop", + onClick: (e) => { + if (e.target === e.currentTarget) { + actions.closeModal(); + } + }, + }, + h( + "div", + { className: "card org-modal-card" }, + h( + "div", + { className: "org-modal-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, title), + ), + h( + "button", + { + type: "button", + className: "org-modal-close", + onClick: () => actions.closeModal(), + "aria-label": "Close dialog", + }, + "x", + ), + ), + body, + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/overviewCard.css b/arma/ui/apps/portal/components/overviewCard.css new file mode 100644 index 0000000..e961dde --- /dev/null +++ b/arma/ui/apps/portal/components/overviewCard.css @@ -0,0 +1,58 @@ +.org-hero-grid { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 1.5rem; + align-items: start; +} + +.org-summary { + margin: 0; + font-size: 1.05rem; + color: var(--text-main); +} + +.org-meta-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.org-meta-item { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(241 245 249) 0%, + rgb(226 232 240) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +.org-meta-label { + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.org-meta-value { + font-size: 1rem; + font-weight: 600; + color: var(--primary-hover); +} + +@media (max-width: 960px) { + .org-hero-grid, + .org-meta-row { + grid-template-columns: 1fr; + } +} diff --git a/arma/ui/apps/portal/components/overviewCard.js b/arma/ui/apps/portal/components/overviewCard.js new file mode 100644 index 0000000..f8aa4b3 --- /dev/null +++ b/arma/ui/apps/portal/components/overviewCard.js @@ -0,0 +1,118 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.OverviewCard = function OverviewCard() { + const MetricCard = OrgPortal.componentFns.MetricCard; + + return h( + "section", + { className: "card org-panel org-span-12" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("div", { className: "org-eyebrow" }, portalData.org.tag), + h( + "h2", + { className: "org-panel-title" }, + "Organization Overview", + ), + ), + ), + h( + "div", + { className: "org-hero-grid" }, + h( + "div", + { className: "org-hero-copy" }, + h( + "p", + { className: "org-summary" }, + portalData.org.type, + " operating from ", + portalData.org.headquarters, + ". Treasury, fleet status, inventory, and roster management are surfaced here first.", + ), + h( + "div", + { className: "org-meta-row" }, + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Director", + ), + h( + "span", + { className: "org-meta-value" }, + portalData.org.owner, + ), + ), + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Active Members", + ), + h( + "span", + { className: "org-meta-value" }, + `${store.getMembers().length} total`, + ), + ), + h( + "div", + { className: "org-meta-item" }, + h( + "span", + { className: "org-meta-label" }, + "Fleet Readiness", + ), + h( + "span", + { className: "org-meta-value" }, + `${actions.getAssetReadiness()}%`, + ), + ), + ), + ), + h( + "div", + { className: "org-metric-grid" }, + MetricCard( + "Org Funds", + actions.formatCurrency(store.getFunds()), + "Organization treasury balance", + ), + MetricCard( + "Reputation", + portalData.reputation, + "Organization standing", + ), + MetricCard( + "Asset Lines", + portalData.assets.length, + "Tracked supply and equipment entries", + ), + MetricCard( + "Fleet Vehicles", + portalData.fleet.length, + "Tracked air, ground, and naval vehicles", + ), + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/portalHeader.css b/arma/ui/apps/portal/components/portalHeader.css new file mode 100644 index 0000000..eaafcae --- /dev/null +++ b/arma/ui/apps/portal/components/portalHeader.css @@ -0,0 +1,41 @@ +.org-page-header { + text-align: left; + margin-bottom: 0; +} + +.org-page-heading { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.org-page-kicker { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; +} + +.org-page-title { + margin: 0; +} + +.org-page-subtitle { + font-size: 0.9rem; + color: var(--text-muted); + margin: 0; +} + +.org-page-meta { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@media (max-width: 960px) { + .org-page-heading { + gap: 0.3rem; + } +} diff --git a/arma/ui/apps/portal/components/portalHeader.js b/arma/ui/apps/portal/components/portalHeader.js new file mode 100644 index 0000000..9ec5cf0 --- /dev/null +++ b/arma/ui/apps/portal/components/portalHeader.js @@ -0,0 +1,30 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData, session } = OrgPortal.data; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.PortalHeader = function PortalHeader() { + return h( + "section", + { className: "card org-panel org-span-12 org-page-header" }, + h( + "div", + { className: "org-page-heading" }, + h("span", { className: "org-page-kicker" }, portalData.org.tag), + h("h1", { className: "org-page-title" }, portalData.org.name), + h( + "p", + { className: "org-page-subtitle" }, + "Player organization command portal", + ), + h( + "span", + { className: "org-page-meta" }, + `${session.actorName} - ${session.role}`, + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/simpleList.css b/arma/ui/apps/portal/components/simpleList.css new file mode 100644 index 0000000..be49ccc --- /dev/null +++ b/arma/ui/apps/portal/components/simpleList.css @@ -0,0 +1,67 @@ +.org-simple-list, +.org-name-list, +.org-activity-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; +} + +.org-simple-list, +.org-name-list, +.org-activity-list, +.org-roadmap-grid { + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +.org-simple-row, +.org-name-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +.org-simple-name { + color: var(--primary-hover); +} + +.org-simple-meta { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 1rem; +} + +.org-name-row { + justify-content: flex-start; + + button { + margin-left: auto; + } +} + +@media (max-width: 960px) { + .org-simple-row, + .org-name-row { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/ui/apps/portal/components/simpleStat.css b/arma/ui/apps/portal/components/simpleStat.css new file mode 100644 index 0000000..824ad98 --- /dev/null +++ b/arma/ui/apps/portal/components/simpleStat.css @@ -0,0 +1,18 @@ +.org-simple-stat { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 90px; +} + +.org-simple-label { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.org-simple-value { + font-size: 0.95rem; + color: var(--text-main); +} diff --git a/arma/ui/apps/portal/components/simpleStat.js b/arma/ui/apps/portal/components/simpleStat.js new file mode 100644 index 0000000..9922ac8 --- /dev/null +++ b/arma/ui/apps/portal/components/simpleStat.js @@ -0,0 +1,15 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { + return h( + "div", + { className: "org-simple-stat" }, + h("span", { className: "org-simple-label" }, label), + h("strong", { className: "org-simple-value" }, value), + ); + }; +})(); diff --git a/arma/ui/apps/portal/components/treasuryCard.css b/arma/ui/apps/portal/components/treasuryCard.css new file mode 100644 index 0000000..26a4417 --- /dev/null +++ b/arma/ui/apps/portal/components/treasuryCard.css @@ -0,0 +1,99 @@ +.org-finance-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; + + > div { + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + display: flex; + flex-direction: column; + gap: 0.4rem; + } +} + +.org-action-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; + + button + button { + margin-left: 0; + } + + button { + width: 100%; + } +} + +.org-treasury-notice { + margin-bottom: 1rem; + padding: 0.85rem 1rem; + border-radius: var(--radius); + font-size: 0.92rem; + + &.is-success { + background: #ecfdf5; + border: 1px solid #bbf7d0; + color: #166534; + } + + &.is-error { + background: #fef2f2; + border: 1px solid #fecaca; + color: #991b1b; + } +} + +.org-credit-lines { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.org-access-note { + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.95rem; +} + +.org-credit-list { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.org-credit-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + + &:nth-child(even) { + background: linear-gradient( + 180deg, + rgb(248 250 252) 0%, + rgb(241 245 249) 100% + ); + border-color: rgb(148 163 184 / 0.45); + } +} + +@media (max-width: 960px) { + .org-finance-meta { + grid-template-columns: 1fr; + } + + .org-credit-row { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/arma/ui/apps/portal/components/treasuryCard.js b/arma/ui/apps/portal/components/treasuryCard.js new file mode 100644 index 0000000..a280783 --- /dev/null +++ b/arma/ui/apps/portal/components/treasuryCard.js @@ -0,0 +1,130 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { + const notice = store.getTreasuryNotice(); + const creditLines = store.getCreditLines(); + const noMembers = store.getMembers().length === 0; + const allowTreasuryActions = permissions.canManageTreasury(); + + return h( + "section", + { className: "card org-panel org-span-5" }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + h("h2", { className: "org-panel-title" }, "Treasury"), + h( + "p", + { className: "org-panel-subtitle" }, + "Organization funds, reputation, and member payouts.", + ), + ), + ), + h( + "div", + { className: "org-finance-meta" }, + h( + "div", + null, + h("span", { className: "org-meta-label" }, "Funds"), + h("strong", null, actions.formatCurrency(store.getFunds())), + ), + h( + "div", + null, + h("span", { className: "org-meta-label" }, "Reputation"), + h("strong", null, `${portalData.reputation}`), + ), + ), + allowTreasuryActions + ? h( + "div", + { className: "org-action-grid" }, + h( + "button", + { + type: "button", + onClick: () => actions.openModal("payroll"), + disabled: noMembers, + }, + "Run Payroll", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.openModal("transfer"), + disabled: noMembers, + }, + "Send Funds", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.openModal("credit"), + disabled: noMembers, + }, + "Credit Line", + ), + ) + : h( + "p", + { className: "org-access-note" }, + "Only the organization leader or CEO can manage treasury actions.", + ), + notice.text + ? h( + "div", + { + className: + notice.type === "error" + ? "org-treasury-notice is-error" + : "org-treasury-notice is-success", + }, + notice.text, + ) + : null, + creditLines.length > 0 + ? h( + "div", + { className: "org-credit-lines" }, + h( + "span", + { className: "org-meta-label" }, + "Active Credit Lines", + ), + h( + "div", + { className: "org-credit-list" }, + ...creditLines.map((line) => + h( + "div", + { className: "org-credit-row" }, + h("span", null, line.member), + h( + "strong", + null, + actions.formatCurrency(line.amount), + ), + ), + ), + ), + ) + : null, + ); + }; +})(); diff --git a/arma/ui/apps/portal/data.js b/arma/ui/apps/portal/data.js new file mode 100644 index 0000000..484aaa0 --- /dev/null +++ b/arma/ui/apps/portal/data.js @@ -0,0 +1,118 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + + OrgPortal.data = { + portalData: { + org: { + name: "Black Rifle Company", + tag: "BRC-0160566824", + type: "Private Military Company", + status: "Operational", + headquarters: "Georgetown Command Annex", + owner: "Jacob Schmidt", + }, + funds: 482750, + reputation: 72, + members: [ + { name: "Jacob Schmidt" }, + { name: "Mara Velez" }, + { name: "Rylan Cross" }, + { name: "Noah Briggs" }, + { name: "Elena Price" }, + { name: "Isaac Rowe" }, + { name: "Talia Boone" }, + { name: "Cade Mercer" }, + ], + fleet: [ + { + name: "UH-80 Ghost Hawk", + type: "helicopter", + status: "Ready", + damage: "16%", + }, + { + name: "MH-9 Hummingbird", + type: "helicopter", + status: "Ready", + damage: "8%", + }, + { + name: "M-ATV Patrol 1", + type: "car", + status: "Fielded", + damage: "24%", + }, + { + name: "M2A1 Slammer", + type: "armor", + status: "Ready", + damage: "11%", + }, + { + name: "RHIB Patrol Boat", + type: "naval", + status: "Repairing", + damage: "32%", + }, + ], + assets: [ + { name: "First Aid Kits", type: "items", quantity: "36" }, + { name: "MX 6.5 mm Rifles", type: "weapons", quantity: "18" }, + { + name: "6.5 mm Magazines", + type: "magazines", + quantity: "120", + }, + { + name: "Carryall Backpacks", + type: "backpacks", + quantity: "24", + }, + ], + activity: [ + { + time: "08:20", + text: "Treasury cleared contractor payment for northern route escort.", + }, + { + time: "07:45", + text: "Viper Flight completed readiness checks on all rotary assets.", + }, + { + time: "07:10", + text: "New recruit Cade Mercer accepted into ground training roster.", + }, + { + time: "06:30", + text: "North Depot inventory count pushed reserve ratio above target.", + }, + ], + roadmap: [ + { + name: "Contracts Board", + status: "Planned", + detail: "Track payouts, assignments, and claim approvals.", + }, + { + name: "Diplomacy", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Logistics Queue", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Permissions", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + ], + }, + session: { + actorName: "Jacob Schmidt", + role: "Leader", + }, + }; +})(); diff --git a/arma/ui/apps/portal/permissions.js b/arma/ui/apps/portal/permissions.js new file mode 100644 index 0000000..7de691e --- /dev/null +++ b/arma/ui/apps/portal/permissions.js @@ -0,0 +1,28 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { portalData, session } = OrgPortal.data; + + class OrgPortalPermissions { + isOrgLeaderOrCeo() { + return ( + session.actorName === portalData.org.owner || + session.role === "Leader" || + session.role === "CEO" + ); + } + + canManageMembers() { + return this.isOrgLeaderOrCeo(); + } + + canManageTreasury() { + return this.isOrgLeaderOrCeo(); + } + + canDisbandOrg() { + return this.isOrgLeaderOrCeo(); + } + } + + OrgPortal.permissions = new OrgPortalPermissions(); +})(); diff --git a/arma/ui/apps/portal/runtime.js b/arma/ui/apps/portal/runtime.js new file mode 100644 index 0000000..8798b3b --- /dev/null +++ b/arma/ui/apps/portal/runtime.js @@ -0,0 +1,98 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + + const SVG_NS = "http://www.w3.org/2000/svg"; + const SVG_TAGS = new Set([ + "svg", + "path", + "circle", + "rect", + "line", + "polyline", + "polygon", + "g", + "defs", + "use", + "text", + "tspan", + "clipPath", + "mask", + ]); + + function h(tag, props = {}, ...children) { + const isSvg = SVG_TAGS.has(tag); + const el = isSvg + ? document.createElementNS(SVG_NS, tag) + : document.createElement(tag); + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith("on") && typeof value === "function") { + el.addEventListener(key.substring(2).toLowerCase(), value); + } else if (key === "className") { + if (isSvg) { + el.setAttribute("class", value); + } else { + el.className = value; + } + } else if (key === "style" && typeof value === "object") { + Object.assign(el.style, value); + } else if (typeof value === "boolean") { + if (value) { + el.setAttribute(key, ""); + } else { + el.removeAttribute(key); + } + } else if (value === null || value === undefined) { + el.removeAttribute(key); + } else { + el.setAttribute(key, value); + } + }); + } + + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { + el.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + el.appendChild(child); + } else if (Array.isArray(child)) { + child.forEach((c) => el.appendChild(c)); + } + }); + + return el; + } + + let rootContainer = null; + let rootComponent = null; + + function render(component, container) { + rootContainer = container; + rootComponent = component; + rerender(); + } + + function rerender() { + rootContainer.innerHTML = ""; + rootContainer.appendChild(rootComponent()); + } + + function createSignal(initialValue) { + let value = initialValue; + + const getValue = () => value; + const setValue = (newValue) => { + value = typeof newValue === "function" ? newValue(value) : newValue; + rerender(); + }; + + return [getValue, setValue]; + } + + OrgPortal.runtime = { + h, + render, + createSignal, + }; +})(); diff --git a/arma/ui/apps/portal/store.js b/arma/ui/apps/portal/store.js new file mode 100644 index 0000000..cb882b6 --- /dev/null +++ b/arma/ui/apps/portal/store.js @@ -0,0 +1,23 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { createSignal } = window.RegistryApp.runtime; + const { portalData } = OrgPortal.data; + + class OrgPortalStore { + constructor() { + [this.getFunds, this.setFunds] = createSignal(portalData.funds); + [this.getMembers, this.setMembers] = createSignal([ + ...portalData.members, + ]); + [this.getCreditLines, this.setCreditLines] = createSignal([]); + [this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({ + type: "", + text: "", + }); + [this.getModal, this.setModal] = createSignal(null); + [this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false); + } + } + + OrgPortal.store = new OrgPortalStore(); +})(); diff --git a/arma/ui/index.html b/arma/ui/index.html deleted file mode 100644 index dc62f47..0000000 --- a/arma/ui/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - ORBIS - Global Organization Network - - - - -
- - - - diff --git a/arma/ui/style.css b/arma/ui/style.css index fe13bdd..550732d 100644 --- a/arma/ui/style.css +++ b/arma/ui/style.css @@ -80,7 +80,6 @@ main { border: 1px solid var(--border); border-radius: var(--radius); padding: 2rem; - margin-bottom: 2rem; box-shadow: var(--shadow); text-align: center;