Skip to content

Commit 96e520d

Browse files
committed
feat: add institution profile administration section
1 parent 5193a5a commit 96e520d

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

src/institution.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"institutionName": "",
3+
"streetAddress": "",
4+
"city": "",
5+
"state": "",
6+
"postalCode": "",
7+
"country": "",
8+
"defaultCurrency": "USD",
9+
"departmentName": "",
10+
"costCenter": "",
11+
"logo": ""
12+
}

src/slurmcostmanager.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ nav button:hover {
136136
box-sizing: border-box;
137137
}
138138

139+
.institution-profile div {
140+
margin-top: 0.5em;
141+
}
142+
143+
.institution-logo-preview {
144+
max-width: 200px;
145+
display: block;
146+
margin-top: 0.5em;
147+
}
148+
139149
@media (max-width: 600px) {
140150
nav button {
141151
display: block;

src/slurmcostmanager.js

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,258 @@ function Details({
954954
}
955955

956956

957+
function InstitutionProfile() {
958+
const [profile, setProfile] = useState({
959+
institutionName: '',
960+
streetAddress: '',
961+
city: '',
962+
state: '',
963+
postalCode: '',
964+
country: '',
965+
defaultCurrency: 'USD',
966+
departmentName: '',
967+
costCenter: '',
968+
logo: ''
969+
});
970+
const [status, setStatus] = useState(null);
971+
const [error, setError] = useState(null);
972+
const baseDir = PLUGIN_BASE;
973+
974+
useEffect(() => {
975+
let cancelled = false;
976+
async function load() {
977+
try {
978+
let text;
979+
if (window.cockpit && window.cockpit.file) {
980+
text = await window.cockpit.file(`${baseDir}/institution.json`).read();
981+
} else {
982+
const resp = await fetch('institution.json');
983+
if (!resp.ok) throw new Error('Failed to load profile');
984+
text = await resp.text();
985+
}
986+
if (cancelled) return;
987+
const json = JSON.parse(text);
988+
setProfile(p => ({ ...p, ...json }));
989+
} catch (e) {
990+
console.error(e);
991+
}
992+
}
993+
load();
994+
return () => {
995+
cancelled = true;
996+
};
997+
}, []);
998+
999+
function update(field, value) {
1000+
setProfile(prev => ({ ...prev, [field]: value }));
1001+
}
1002+
1003+
function handleLogo(e) {
1004+
const file = e.target.files && e.target.files[0];
1005+
if (!file) return;
1006+
const reader = new FileReader();
1007+
reader.onload = () => {
1008+
setProfile(prev => ({ ...prev, logo: reader.result }));
1009+
};
1010+
reader.readAsDataURL(file);
1011+
}
1012+
1013+
async function save() {
1014+
try {
1015+
setStatus(null);
1016+
setError(null);
1017+
const text = JSON.stringify(profile, null, 2);
1018+
if (window.cockpit && window.cockpit.file) {
1019+
await window.cockpit.file(`${baseDir}/institution.json`).replace(text);
1020+
} else {
1021+
await fetch('institution.json', {
1022+
method: 'PUT',
1023+
headers: { 'Content-Type': 'application/json' },
1024+
body: text
1025+
});
1026+
}
1027+
setStatus('Saved');
1028+
} catch (e) {
1029+
console.error(e);
1030+
setError('Failed to save profile');
1031+
}
1032+
}
1033+
1034+
return React.createElement(
1035+
'div',
1036+
{ className: 'institution-profile' },
1037+
React.createElement('h2', null, 'Institution Profile'),
1038+
React.createElement(
1039+
'p',
1040+
null,
1041+
"Enter your university's name, contact details, and other key information"
1042+
),
1043+
React.createElement(
1044+
'div',
1045+
null,
1046+
React.createElement(
1047+
'label',
1048+
null,
1049+
'Logo: ',
1050+
React.createElement('input', {
1051+
type: 'file',
1052+
accept: 'image/*',
1053+
onChange: handleLogo
1054+
})
1055+
),
1056+
profile.logo &&
1057+
React.createElement('img', {
1058+
src: profile.logo,
1059+
alt: 'Logo',
1060+
className: 'institution-logo-preview'
1061+
})
1062+
),
1063+
React.createElement(
1064+
'div',
1065+
null,
1066+
React.createElement(
1067+
'label',
1068+
null,
1069+
'Institution Name: ',
1070+
React.createElement('input', {
1071+
type: 'text',
1072+
value: profile.institutionName,
1073+
onChange: e => update('institutionName', e.target.value)
1074+
})
1075+
)
1076+
),
1077+
React.createElement(
1078+
'div',
1079+
null,
1080+
React.createElement(
1081+
'label',
1082+
null,
1083+
'Street Address: ',
1084+
React.createElement('input', {
1085+
type: 'text',
1086+
value: profile.streetAddress,
1087+
onChange: e => update('streetAddress', e.target.value)
1088+
})
1089+
)
1090+
),
1091+
React.createElement(
1092+
'div',
1093+
null,
1094+
React.createElement(
1095+
'label',
1096+
null,
1097+
'City: ',
1098+
React.createElement('input', {
1099+
type: 'text',
1100+
value: profile.city,
1101+
onChange: e => update('city', e.target.value)
1102+
})
1103+
)
1104+
),
1105+
React.createElement(
1106+
'div',
1107+
null,
1108+
React.createElement(
1109+
'label',
1110+
null,
1111+
'State/Province: ',
1112+
React.createElement('input', {
1113+
type: 'text',
1114+
value: profile.state,
1115+
onChange: e => update('state', e.target.value)
1116+
})
1117+
)
1118+
),
1119+
React.createElement(
1120+
'div',
1121+
null,
1122+
React.createElement(
1123+
'label',
1124+
null,
1125+
'Postal Code: ',
1126+
React.createElement('input', {
1127+
type: 'text',
1128+
value: profile.postalCode,
1129+
onChange: e => update('postalCode', e.target.value)
1130+
})
1131+
)
1132+
),
1133+
React.createElement(
1134+
'div',
1135+
null,
1136+
React.createElement(
1137+
'label',
1138+
null,
1139+
'Country: ',
1140+
React.createElement('input', {
1141+
type: 'text',
1142+
value: profile.country,
1143+
onChange: e => update('country', e.target.value)
1144+
})
1145+
)
1146+
),
1147+
React.createElement(
1148+
'div',
1149+
null,
1150+
React.createElement(
1151+
'label',
1152+
null,
1153+
'Default Currency: ',
1154+
React.createElement(
1155+
'select',
1156+
{
1157+
value: profile.defaultCurrency,
1158+
onChange: e => update('defaultCurrency', e.target.value)
1159+
},
1160+
['USD', 'EUR', 'GBP', 'CAD', 'AUD'].map(c =>
1161+
React.createElement('option', { key: c, value: c }, c)
1162+
)
1163+
)
1164+
)
1165+
),
1166+
React.createElement(
1167+
'div',
1168+
null,
1169+
React.createElement(
1170+
'label',
1171+
null,
1172+
'Recharge Unit/Department Name: ',
1173+
React.createElement('input', {
1174+
type: 'text',
1175+
value: profile.departmentName,
1176+
onChange: e => update('departmentName', e.target.value)
1177+
})
1178+
)
1179+
),
1180+
React.createElement(
1181+
'div',
1182+
null,
1183+
React.createElement(
1184+
'label',
1185+
null,
1186+
'Recharge Cost Center / Chart String: ',
1187+
React.createElement('input', {
1188+
type: 'text',
1189+
value: profile.costCenter,
1190+
onChange: e => update('costCenter', e.target.value)
1191+
})
1192+
)
1193+
),
1194+
React.createElement(
1195+
'div',
1196+
{ style: { marginTop: '1em' } },
1197+
React.createElement('button', { onClick: save }, 'Save'),
1198+
status && React.createElement('span', { style: { marginLeft: '0.5em' } }, status),
1199+
error &&
1200+
React.createElement(
1201+
'span',
1202+
{ className: 'error', style: { marginLeft: '0.5em' } },
1203+
error
1204+
)
1205+
)
1206+
);
1207+
}
1208+
9571209
function Rates({ onRatesUpdated }) {
9581210
const [config, setConfig] = useState(null);
9591211
const [overrides, setOverrides] = useState([]);
@@ -1117,6 +1369,7 @@ function Rates({ onRatesUpdated }) {
11171369
return React.createElement(
11181370
'div',
11191371
null,
1372+
React.createElement(InstitutionProfile, null),
11201373
React.createElement('h2', null, 'Rate Configuration'),
11211374
React.createElement(
11221375
'div',

0 commit comments

Comments
 (0)