Refactor org UI into single app and addon site
This commit is contained in:
parent
a373943eca
commit
aad0fc61c4
175
arma/client/addons/org/ui/_site/base.css
Normal file
175
arma/client/addons/org/ui/_site/base.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
6
arma/client/addons/org/ui/_site/bootstrap.js
vendored
Normal file
6
arma/client/addons/org/ui/_site/bootstrap.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Registry app bootstrap
|
||||
*/
|
||||
|
||||
const root = document.getElementById("app");
|
||||
window.RegistryApp.runtime.render(window.RegistryApp.components.App, root);
|
||||
168
arma/client/addons/org/ui/_site/components/createOrgForm.js
Normal file
168
arma/client/addons/org/ui/_site/components/createOrgForm.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
43
arma/client/addons/org/ui/_site/components/footer.js
Normal file
43
arma/client/addons/org/ui/_site/components/footer.js
Normal file
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
72
arma/client/addons/org/ui/_site/components/forms.css
Normal file
72
arma/client/addons/org/ui/_site/components/forms.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
23
arma/client/addons/org/ui/_site/components/header.js
Normal file
23
arma/client/addons/org/ui/_site/components/header.js
Normal file
@ -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"),
|
||||
);
|
||||
};
|
||||
})();
|
||||
12
arma/client/addons/org/ui/_site/components/homeView.css
Normal file
12
arma/client/addons/org/ui/_site/components/homeView.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
57
arma/client/addons/org/ui/_site/components/homeView.js
Normal file
57
arma/client/addons/org/ui/_site/components/homeView.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
48
arma/client/addons/org/ui/_site/components/index.js
Normal file
48
arma/client/addons/org/ui/_site/components/index.js
Normal file
@ -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(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
76
arma/client/addons/org/ui/_site/components/loginForm.js
Normal file
76
arma/client/addons/org/ui/_site/components/loginForm.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
79
arma/client/addons/org/ui/_site/components/navbar.css
Normal file
79
arma/client/addons/org/ui/_site/components/navbar.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
70
arma/client/addons/org/ui/_site/components/navbar.js
Normal file
70
arma/client/addons/org/ui/_site/components/navbar.js
Normal file
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,243 +1,109 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ORBIS - Global Organization Network</title>
|
||||
<script>
|
||||
const addonRoot = "forge\\forge_client\\addons\\org\\ui\\_site\\";
|
||||
const styleFiles = [
|
||||
"base.css",
|
||||
"components\\navbar.css",
|
||||
"components\\homeView.css",
|
||||
"components\\forms.css",
|
||||
"portal\\components\\controls.css",
|
||||
"portal\\components\\layout.css",
|
||||
"portal\\components\\portalHeader.css",
|
||||
"portal\\components\\overviewCard.css",
|
||||
"portal\\components\\metricCard.css",
|
||||
"portal\\components\\simpleList.css",
|
||||
"portal\\components\\simpleStat.css",
|
||||
"portal\\components\\treasuryCard.css",
|
||||
"portal\\components\\activityCard.css",
|
||||
"portal\\components\\futureCard.css",
|
||||
"portal\\components\\dangerCard.css",
|
||||
"portal\\components\\modalLayer.css",
|
||||
];
|
||||
const scriptFiles = [
|
||||
"runtime.js",
|
||||
"state.js",
|
||||
"portal\\runtime.js",
|
||||
"portal\\data.js",
|
||||
"portal\\store.js",
|
||||
"portal\\permissions.js",
|
||||
"portal\\actions.js",
|
||||
"portal\\components\\metricCard.js",
|
||||
"portal\\components\\simpleStat.js",
|
||||
"portal\\components\\portalHeader.js",
|
||||
"portal\\components\\overviewCard.js",
|
||||
"portal\\components\\fleetCard.js",
|
||||
"portal\\components\\treasuryCard.js",
|
||||
"portal\\components\\assetsCard.js",
|
||||
"portal\\components\\membersCard.js",
|
||||
"portal\\components\\activityCard.js",
|
||||
"portal\\components\\futureCard.js",
|
||||
"portal\\components\\dangerCard.js",
|
||||
"portal\\components\\modalLayer.js",
|
||||
"portal\\components\\disbandedView.js",
|
||||
"portal\\components\\footer.js",
|
||||
"portal\\components\\index.js",
|
||||
"components\\navbar.js",
|
||||
"components\\header.js",
|
||||
"components\\loginForm.js",
|
||||
"components\\createOrgForm.js",
|
||||
"components\\homeView.js",
|
||||
"components\\footer.js",
|
||||
"components\\index.js",
|
||||
"bootstrap.js",
|
||||
];
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Organization Dashboard</title>
|
||||
<!-- <link rel="stylesheet" href="style.css" /> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
This approach is used instead of static HTML imports to work with Arma 3's file system
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\org\\ui\\_site\\style.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\org\\ui\\_site\\script.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
function requestText(path) {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(addonRoot + path);
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
return fetch(path).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load " + path);
|
||||
}
|
||||
|
||||
<body>
|
||||
<div class="dashboard-container">
|
||||
<!-- Header Section -->
|
||||
<div class="dashboard-header">
|
||||
<div class="org-logo">
|
||||
<div class="logo-placeholder">ORG</div>
|
||||
</div>
|
||||
<div class="org-info">
|
||||
<h1 class="org-name">Organization Name</h1>
|
||||
<p class="org-tag">FACTION-001</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="action-btn">Settings</button>
|
||||
<button class="action-btn close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="dashboard-grid">
|
||||
<!-- Overview Card -->
|
||||
<div class="dashboard-card card-wide">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Overview</h2>
|
||||
<div class="card-status">Active</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="stat-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Members</span>
|
||||
<span class="stat-value">24</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Online Now</span>
|
||||
<span class="stat-value">8</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Org Balance</span>
|
||||
<span class="stat-value">$125,000</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Reputation</span>
|
||||
<span class="stat-value">200</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
function appendStyle(css) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
<!-- Members Card -->
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Members Online</h2>
|
||||
<span class="card-badge">8</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="member-list">
|
||||
<div class="member-item">
|
||||
<div class="member-status online"></div>
|
||||
<div class="member-info">
|
||||
<span class="member-name">John Doe</span>
|
||||
<span class="member-rank">Leader</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-item">
|
||||
<div class="member-status online"></div>
|
||||
<div class="member-info">
|
||||
<span class="member-name">Jane Smith</span>
|
||||
<span class="member-rank">Officer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-item">
|
||||
<div class="member-status online"></div>
|
||||
<div class="member-info">
|
||||
<span class="member-name">Mike Johnson</span>
|
||||
<span class="member-rank">Member</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-item">
|
||||
<div class="member-status online"></div>
|
||||
<div class="member-info">
|
||||
<span class="member-name">Sarah Wilson</span>
|
||||
<span class="member-rank">Member</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
<!-- Recent Activity Card -->
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Recent Activity</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="activity-list">
|
||||
<div class="activity-item">
|
||||
<div class="activity-time">2m ago</div>
|
||||
<div class="activity-text">Mike Johnson completed mission "Alpha Strike"</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-time">15m ago</div>
|
||||
<div class="activity-text">Jane Smith deposited $5,000 to org bank</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-time">1h ago</div>
|
||||
<div class="activity-text">New member Alex Brown joined the organization</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-time">2h ago</div>
|
||||
<div class="activity-text">Organization captured territory: Zone-7</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Card -->
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Assets</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="asset-list">
|
||||
<div class="asset-item">
|
||||
<span class="asset-icon">🏢</span>
|
||||
<div class="asset-info">
|
||||
<span class="asset-name">Headquarters</span>
|
||||
<span class="asset-location">Downtown</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-item">
|
||||
<span class="asset-icon">🚁</span>
|
||||
<div class="asset-info">
|
||||
<span class="asset-name">Helicopters</span>
|
||||
<span class="asset-location">3 units</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-item">
|
||||
<span class="asset-icon">🚗</span>
|
||||
<div class="asset-info">
|
||||
<span class="asset-name">Vehicles</span>
|
||||
<span class="asset-location">12 units</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-item">
|
||||
<span class="asset-icon">📦</span>
|
||||
<div class="asset-info">
|
||||
<span class="asset-name">Storage Units</span>
|
||||
<span class="asset-location">5 locations</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missions Card -->
|
||||
<div class="dashboard-card card-wide">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Active Missions</h2>
|
||||
<span class="card-badge">3</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="mission-list">
|
||||
<div class="mission-item">
|
||||
<div class="mission-header">
|
||||
<span class="mission-name">Supply Run</span>
|
||||
<span class="mission-priority high">High Priority</span>
|
||||
</div>
|
||||
<div class="mission-description">Deliver supplies to northern outpost</div>
|
||||
<div class="mission-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 65%"></div>
|
||||
</div>
|
||||
<span class="progress-text">65%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mission-item">
|
||||
<div class="mission-header">
|
||||
<span class="mission-name">Recon Operation</span>
|
||||
<span class="mission-priority medium">Medium Priority</span>
|
||||
</div>
|
||||
<div class="mission-description">Scout enemy positions in Zone-4</div>
|
||||
<div class="mission-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 30%"></div>
|
||||
</div>
|
||||
<span class="progress-text">30%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mission-item">
|
||||
<div class="mission-header">
|
||||
<span class="mission-name">Territory Defense</span>
|
||||
<span class="mission-priority low">Low Priority</span>
|
||||
</div>
|
||||
<div class="mission-description">Maintain control of captured zones</div>
|
||||
<div class="mission-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 90%"></div>
|
||||
</div>
|
||||
<span class="progress-text">90%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
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,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
267
arma/client/addons/org/ui/_site/portal/actions.js
Normal file
267
arma/client/addons/org/ui/_site/portal/actions.js
Normal file
@ -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();
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
43
arma/client/addons/org/ui/_site/portal/components/footer.js
Normal file
43
arma/client/addons/org/ui/_site/portal/components/footer.js
Normal file
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
65
arma/client/addons/org/ui/_site/portal/components/index.js
Normal file
65
arma/client/addons/org/ui/_site/portal/components/index.js
Normal file
@ -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(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
81
arma/client/addons/org/ui/_site/portal/components/layout.css
Normal file
81
arma/client/addons/org/ui/_site/portal/components/layout.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
};
|
||||
})();
|
||||
131
arma/client/addons/org/ui/_site/portal/components/modalLayer.css
Normal file
131
arma/client/addons/org/ui/_site/portal/components/modalLayer.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
286
arma/client/addons/org/ui/_site/portal/components/modalLayer.js
Normal file
286
arma/client/addons/org/ui/_site/portal/components/modalLayer.js
Normal file
@ -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,
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
})();
|
||||
118
arma/client/addons/org/ui/_site/portal/data.js
Normal file
118
arma/client/addons/org/ui/_site/portal/data.js
Normal file
@ -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",
|
||||
},
|
||||
};
|
||||
})();
|
||||
28
arma/client/addons/org/ui/_site/portal/permissions.js
Normal file
28
arma/client/addons/org/ui/_site/portal/permissions.js
Normal file
@ -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();
|
||||
})();
|
||||
98
arma/client/addons/org/ui/_site/portal/runtime.js
Normal file
98
arma/client/addons/org/ui/_site/portal/runtime.js
Normal file
@ -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,
|
||||
};
|
||||
})();
|
||||
23
arma/client/addons/org/ui/_site/portal/store.js
Normal file
23
arma/client/addons/org/ui/_site/portal/store.js
Normal file
@ -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();
|
||||
})();
|
||||
73
arma/client/addons/org/ui/_site/runtime.js
Normal file
73
arma/client/addons/org/ui/_site/runtime.js
Normal file
@ -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,
|
||||
};
|
||||
})();
|
||||
@ -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 = `
|
||||
<div class="member-status ${member.online ? 'online' : 'offline'}"></div>
|
||||
<div class="member-info">
|
||||
<span class="member-name">${member.name}</span>
|
||||
<span class="member-rank">${member.rank}</span>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<span class="asset-icon">${asset.icon || '📦'}</span>
|
||||
<div class="asset-info">
|
||||
<span class="asset-name">${asset.name}</span>
|
||||
<span class="asset-location">${asset.location}</span>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="activity-time">${activity.time}</div>
|
||||
<div class="activity-text">${activity.text}</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="mission-header">
|
||||
<span class="mission-name">${mission.name}</span>
|
||||
<span class="mission-priority ${mission.priority}">${mission.priority.charAt(0).toUpperCase() + mission.priority.slice(1)} Priority</span>
|
||||
</div>
|
||||
<div class="mission-description">${mission.description}</div>
|
||||
<div class="mission-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${mission.progress}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">${mission.progress}%</span>
|
||||
</div>
|
||||
`;
|
||||
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;
|
||||
12
arma/client/addons/org/ui/_site/state.js
Normal file
12
arma/client/addons/org/ui/_site/state.js
Normal file
@ -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();
|
||||
})();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
235
arma/ui/app.js
235
arma/ui/app.js
@ -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);
|
||||
175
arma/ui/apps/base.css
Normal file
175
arma/ui/apps/base.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
6
arma/ui/apps/main/bootstrap.js
vendored
Normal file
6
arma/ui/apps/main/bootstrap.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Registry app bootstrap
|
||||
*/
|
||||
|
||||
const root = document.getElementById("app");
|
||||
window.RegistryApp.runtime.render(window.RegistryApp.components.App, root);
|
||||
168
arma/ui/apps/main/components/createOrgForm.js
Normal file
168
arma/ui/apps/main/components/createOrgForm.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
43
arma/ui/apps/main/components/footer.js
Normal file
43
arma/ui/apps/main/components/footer.js
Normal file
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
72
arma/ui/apps/main/components/forms.css
Normal file
72
arma/ui/apps/main/components/forms.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
23
arma/ui/apps/main/components/header.js
Normal file
23
arma/ui/apps/main/components/header.js
Normal file
@ -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"),
|
||||
);
|
||||
};
|
||||
})();
|
||||
12
arma/ui/apps/main/components/homeView.css
Normal file
12
arma/ui/apps/main/components/homeView.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
57
arma/ui/apps/main/components/homeView.js
Normal file
57
arma/ui/apps/main/components/homeView.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
48
arma/ui/apps/main/components/index.js
Normal file
48
arma/ui/apps/main/components/index.js
Normal file
@ -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(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
76
arma/ui/apps/main/components/loginForm.js
Normal file
76
arma/ui/apps/main/components/loginForm.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
79
arma/ui/apps/main/components/navbar.css
Normal file
79
arma/ui/apps/main/components/navbar.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
70
arma/ui/apps/main/components/navbar.js
Normal file
70
arma/ui/apps/main/components/navbar.js
Normal file
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
58
arma/ui/apps/main/index.html
Normal file
58
arma/ui/apps/main/index.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ORBIS - Global Organization Network</title>
|
||||
<link rel="stylesheet" href="../base.css" />
|
||||
<link rel="stylesheet" href="components/navbar.css" />
|
||||
<link rel="stylesheet" href="components/homeView.css" />
|
||||
<link rel="stylesheet" href="components/forms.css" />
|
||||
<link rel="stylesheet" href="../portal/components/controls.css" />
|
||||
<link rel="stylesheet" href="../portal/components/layout.css" />
|
||||
<link rel="stylesheet" href="../portal/components/portalHeader.css" />
|
||||
<link rel="stylesheet" href="../portal/components/overviewCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/metricCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/simpleList.css" />
|
||||
<link rel="stylesheet" href="../portal/components/simpleStat.css" />
|
||||
<link rel="stylesheet" href="../portal/components/treasuryCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/activityCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/futureCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/dangerCard.css" />
|
||||
<link rel="stylesheet" href="../portal/components/modalLayer.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="runtime.js"></script>
|
||||
<script src="state.js"></script>
|
||||
<script src="../portal/runtime.js"></script>
|
||||
<script src="../portal/data.js"></script>
|
||||
<script src="../portal/store.js"></script>
|
||||
<script src="../portal/permissions.js"></script>
|
||||
<script src="../portal/actions.js"></script>
|
||||
<script src="../portal/components/metricCard.js"></script>
|
||||
<script src="../portal/components/simpleStat.js"></script>
|
||||
<script src="../portal/components/portalHeader.js"></script>
|
||||
<script src="../portal/components/overviewCard.js"></script>
|
||||
<script src="../portal/components/fleetCard.js"></script>
|
||||
<script src="../portal/components/treasuryCard.js"></script>
|
||||
<script src="../portal/components/assetsCard.js"></script>
|
||||
<script src="../portal/components/membersCard.js"></script>
|
||||
<script src="../portal/components/activityCard.js"></script>
|
||||
<script src="../portal/components/futureCard.js"></script>
|
||||
<script src="../portal/components/dangerCard.js"></script>
|
||||
<script src="../portal/components/modalLayer.js"></script>
|
||||
<script src="../portal/components/disbandedView.js"></script>
|
||||
<script src="../portal/components/footer.js"></script>
|
||||
<script src="../portal/components/index.js"></script>
|
||||
<script src="components/navbar.js"></script>
|
||||
<script src="components/header.js"></script>
|
||||
<script src="components/loginForm.js"></script>
|
||||
<script src="components/createOrgForm.js"></script>
|
||||
<script src="components/homeView.js"></script>
|
||||
<script src="components/footer.js"></script>
|
||||
<script src="components/index.js"></script>
|
||||
<script src="bootstrap.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
73
arma/ui/apps/main/runtime.js
Normal file
73
arma/ui/apps/main/runtime.js
Normal file
@ -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,
|
||||
};
|
||||
})();
|
||||
12
arma/ui/apps/main/state.js
Normal file
12
arma/ui/apps/main/state.js
Normal file
@ -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();
|
||||
})();
|
||||
267
arma/ui/apps/portal/actions.js
Normal file
267
arma/ui/apps/portal/actions.js
Normal file
@ -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();
|
||||
})();
|
||||
32
arma/ui/apps/portal/components/activityCard.css
Normal file
32
arma/ui/apps/portal/components/activityCard.css
Normal file
@ -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;
|
||||
}
|
||||
44
arma/ui/apps/portal/components/activityCard.js
Normal file
44
arma/ui/apps/portal/components/activityCard.js
Normal file
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
55
arma/ui/apps/portal/components/assetsCard.js
Normal file
55
arma/ui/apps/portal/components/assetsCard.js
Normal file
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
33
arma/ui/apps/portal/components/controls.css
Normal file
33
arma/ui/apps/portal/components/controls.css
Normal file
@ -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;
|
||||
}
|
||||
22
arma/ui/apps/portal/components/dangerCard.css
Normal file
22
arma/ui/apps/portal/components/dangerCard.css
Normal file
@ -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;
|
||||
}
|
||||
56
arma/ui/apps/portal/components/dangerCard.js
Normal file
56
arma/ui/apps/portal/components/dangerCard.js
Normal file
@ -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",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
47
arma/ui/apps/portal/components/disbandedView.js
Normal file
47
arma/ui/apps/portal/components/disbandedView.js
Normal file
@ -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",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
56
arma/ui/apps/portal/components/fleetCard.js
Normal file
56
arma/ui/apps/portal/components/fleetCard.js
Normal file
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
43
arma/ui/apps/portal/components/footer.js
Normal file
43
arma/ui/apps/portal/components/footer.js
Normal file
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
74
arma/ui/apps/portal/components/futureCard.css
Normal file
74
arma/ui/apps/portal/components/futureCard.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
arma/ui/apps/portal/components/futureCard.js
Normal file
45
arma/ui/apps/portal/components/futureCard.js
Normal file
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
65
arma/ui/apps/portal/components/index.js
Normal file
65
arma/ui/apps/portal/components/index.js
Normal file
@ -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(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
81
arma/ui/apps/portal/components/layout.css
Normal file
81
arma/ui/apps/portal/components/layout.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
75
arma/ui/apps/portal/components/membersCard.js
Normal file
75
arma/ui/apps/portal/components/membersCard.js
Normal file
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
69
arma/ui/apps/portal/components/metricCard.css
Normal file
69
arma/ui/apps/portal/components/metricCard.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
arma/ui/apps/portal/components/metricCard.js
Normal file
20
arma/ui/apps/portal/components/metricCard.js
Normal file
@ -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),
|
||||
);
|
||||
};
|
||||
})();
|
||||
131
arma/ui/apps/portal/components/modalLayer.css
Normal file
131
arma/ui/apps/portal/components/modalLayer.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
286
arma/ui/apps/portal/components/modalLayer.js
Normal file
286
arma/ui/apps/portal/components/modalLayer.js
Normal file
@ -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,
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
58
arma/ui/apps/portal/components/overviewCard.css
Normal file
58
arma/ui/apps/portal/components/overviewCard.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
118
arma/ui/apps/portal/components/overviewCard.js
Normal file
118
arma/ui/apps/portal/components/overviewCard.js
Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
41
arma/ui/apps/portal/components/portalHeader.css
Normal file
41
arma/ui/apps/portal/components/portalHeader.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
30
arma/ui/apps/portal/components/portalHeader.js
Normal file
30
arma/ui/apps/portal/components/portalHeader.js
Normal file
@ -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}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
67
arma/ui/apps/portal/components/simpleList.css
Normal file
67
arma/ui/apps/portal/components/simpleList.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
18
arma/ui/apps/portal/components/simpleStat.css
Normal file
18
arma/ui/apps/portal/components/simpleStat.css
Normal file
@ -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);
|
||||
}
|
||||
15
arma/ui/apps/portal/components/simpleStat.js
Normal file
15
arma/ui/apps/portal/components/simpleStat.js
Normal file
@ -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),
|
||||
);
|
||||
};
|
||||
})();
|
||||
99
arma/ui/apps/portal/components/treasuryCard.css
Normal file
99
arma/ui/apps/portal/components/treasuryCard.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
130
arma/ui/apps/portal/components/treasuryCard.js
Normal file
130
arma/ui/apps/portal/components/treasuryCard.js
Normal file
@ -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,
|
||||
);
|
||||
};
|
||||
})();
|
||||
118
arma/ui/apps/portal/data.js
Normal file
118
arma/ui/apps/portal/data.js
Normal file
@ -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",
|
||||
},
|
||||
};
|
||||
})();
|
||||
28
arma/ui/apps/portal/permissions.js
Normal file
28
arma/ui/apps/portal/permissions.js
Normal file
@ -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();
|
||||
})();
|
||||
98
arma/ui/apps/portal/runtime.js
Normal file
98
arma/ui/apps/portal/runtime.js
Normal file
@ -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,
|
||||
};
|
||||
})();
|
||||
23
arma/ui/apps/portal/store.js
Normal file
23
arma/ui/apps/portal/store.js
Normal file
@ -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();
|
||||
})();
|
||||
@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ORBIS - Global Organization Network</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user