diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php index 0e012f9..dbb2655 100644 --- a/app/Http/Controllers/ApiController.php +++ b/app/Http/Controllers/ApiController.php @@ -173,6 +173,31 @@ public function viewAllInstitutions(Request $request) return ApiController::constructDatakinderRequest($request, '/institutions', 'GET', /* No POST body */ null); } + /** + * GET /institutions/{current inst_id} — details for the institution in session (edit form). + */ + public function getCurrentInstitutionDetails(Request $request) + { + if (ApiController::isLocalRequest()) { + return response()->json([ + 'inst_id' => (string) ($request->attributes->get('inst_id') ?? ''), + 'name' => 'Mock University', + 'state' => 'NY', + 'pdp_id' => '12345', + 'edvise_id' => null, + 'legacy_id' => null, + 'retention_days' => null, + ], 200); + } + + $resp = ApiController::constructInstRequest($request, '', 'GET', null); + if ($resp instanceof \Illuminate\Http\JsonResponse) { + return $resp; + } + + return response()->json($resp->json(), $resp->status()); + } + /** * HTTP client timeout (seconds) when proxying institution requests to BACKEND_URL. * Validate-upload needs a long wait for large CSV processing; other paths stay short. diff --git a/app/Http/Middleware/RequireInstitution.php b/app/Http/Middleware/RequireInstitution.php index b10d620..7b83b3a 100644 --- a/app/Http/Middleware/RequireInstitution.php +++ b/app/Http/Middleware/RequireInstitution.php @@ -11,7 +11,7 @@ class RequireInstitution { private const SKIP_ACTIONS = [ - 'createInstApi', 'addDatakinderApi', 'viewAllInstitutions', 'EditInstApi', + 'createInstApi', 'addDatakinderApi', 'viewAllInstitutions', ]; /** Route names (and patterns) that do not require an institution. */ diff --git a/resources/js/Pages/CreateInst.jsx b/resources/js/Pages/CreateInst.jsx index 32d8b1b..b091b5a 100644 --- a/resources/js/Pages/CreateInst.jsx +++ b/resources/js/Pages/CreateInst.jsx @@ -1,33 +1,21 @@ import React, { useState } from 'react'; import AppLayout from '@/Layouts/AppLayout'; -import { TrashIcon } from '@heroicons/react/24/outline'; import axios from 'axios'; import HeaderLabel from '@/Components/HeaderLabel'; import { Cog8ToothIcon } from '@heroicons/react/24/outline'; -export default function CreateInst() { - // TODO: switch to error instead of setting result_area - const [error, setError] = useState(null); - - // Any update to this schemas list needs to be reflected in the handleSubmit() function call as a checkbox. - const schemas = [ - { name: 'Custom', selected: false }, - { name: 'PDP', selected: false }, - { name: 'Edvise', selected: false }, - { name: 'Legacy', selected: false }, - ]; +const SCHOOL_TYPES = [ + { value: 'pdp', label: 'PDP' }, + { value: 'edvise', label: 'Edvise' }, + { value: 'legacy', label: 'Legacy' }, +]; +export default function CreateInst() { + const [schoolType, setSchoolType] = useState(''); const [addUserCounter, setAddUserCounter] = useState(0); - // TODO implement remove additional email fields - const removeItem = itemId => { - const emailItem = document.getElementById(itemId); - emailItem.remove; - }; - const incrementCounter = () => { - const newId = addUserCounter + 1; - setAddUserCounter(newId); + setAddUserCounter(c => c + 1); }; const renderFullEmailList = () => { @@ -75,7 +63,6 @@ export default function CreateInst() { const handleSubmit = event => { event.preventDefault(); - let pdp = event.target.elements.PDP.checked; if ( event.target.elements.inst_name.value == null || event.target.elements.inst_name.value == '' @@ -84,30 +71,19 @@ export default function CreateInst() { 'Error: Institution name is required.'; return; } - // Enforce the selection of at least one schema type. - const edvise = event.target.elements.Edvise?.checked ?? false; - const legacy = event.target.elements.Legacy?.checked ?? false; - if ( - !event.target.elements.Custom.checked && - !event.target.elements.PDP.checked && - !edvise && - !legacy - ) { + if (!schoolType) { document.getElementById('result_area').innerHTML = - 'Error: Schema type must contain at least one selection.'; + 'Error: Select exactly one of PDP, Edvise, or Legacy.'; return; } - // At most one of PDP, Edvise, or Legacy (API allows only one school type). - const schoolTypeCount = [event.target.elements.PDP.checked, edvise, legacy].filter(Boolean).length; - if (schoolTypeCount > 1) { - document.getElementById('result_area').innerHTML = - 'Error: Select at most one of PDP, Edvise, or Legacy.'; - return; + if (schoolType === 'pdp') { + const raw = event.target.elements.pdp_id?.value?.trim() ?? ''; + if (!raw) { + document.getElementById('result_area').innerHTML = + 'Error: PDP Institution ID is required when PDP is selected.'; + return; + } } - // We currently only have custom for potential other schemas. Note that the schema passed to the API call must match the corresponding backend schema enum value. - let other_schemas = event.target.elements.Custom.checked - ? ['UNKNOWN'] - : null; var emailDict = {}; var accessDict = {}; Array.from(event.target.elements).forEach(input => { @@ -127,17 +103,21 @@ export default function CreateInst() { constructedEmailDict[value] = accessDict[key]; } } + const pdpChecked = schoolType === 'pdp'; const payload = { name: event.target.elements.inst_name.value, state: event.target.elements.state.value, - allowed_schemas: other_schemas, allowed_emails: - constructedEmailDict.length == 0 ? null : constructedEmailDict, - is_pdp: pdp, - pdp_id: event.target.elements.pdp_id?.value || null, + Object.keys(constructedEmailDict).length === 0 + ? null + : constructedEmailDict, + is_pdp: pdpChecked, + pdp_id: pdpChecked + ? (event.target.elements.pdp_id?.value || null) + : null, }; - if (edvise) payload.is_edvise = true; - if (legacy) payload.is_legacy = true; + if (schoolType === 'edvise') payload.is_edvise = true; + if (schoolType === 'legacy') payload.is_legacy = true; return axios({ method: 'post', url: '/create-inst-api', @@ -159,7 +139,6 @@ export default function CreateInst() { }); }; - // TODO check if the user is a datakinder, otherwise show an error page. return ( setSchoolType('')} >
@@ -270,71 +250,58 @@ export default function CreateInst() {
- Schemas accepted by this institution + Institution type -
- {schemas.map((schem, idx) => ( -
-
- - -
-
-
- - - - -
-
+

+ Choose exactly one. Required before submit. +

+
+ {SCHOOL_TYPES.map(({ value, label }) => ( +
+ setSchoolType(value)} + className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600" + /> +
))}
- - -

- For PDP schools, please add the PDP_INST id of the - institution. Include any leading zeroes. -

+ {schoolType === 'pdp' ? ( + <> + + +

+ For PDP schools, please add the PDP_INST id of the + institution. Include any leading zeroes. +

+ + ) : ( +

+ PDP Institution ID applies only when PDP is selected. +

+ )}
diff --git a/resources/js/Pages/EditInst.jsx b/resources/js/Pages/EditInst.jsx index 0c4df4d..5374bcc 100644 --- a/resources/js/Pages/EditInst.jsx +++ b/resources/js/Pages/EditInst.jsx @@ -1,34 +1,73 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import AppLayout from '@/Layouts/AppLayout'; -import { TrashIcon } from '@heroicons/react/24/outline'; import axios from 'axios'; import HeaderLabel from '@/Components/HeaderLabel'; import { Cog8ToothIcon } from '@heroicons/react/24/outline'; +import { Link, usePage } from '@inertiajs/react'; const US_STATES = [ 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', - 'SD', 'TN', 'TX', 'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY' + 'SD', 'TN', 'TX', 'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY', ]; +const SCHOOL_TYPES = [ + { value: 'pdp', label: 'PDP' }, + { value: 'edvise', label: 'Edvise' }, + { value: 'legacy', label: 'Legacy' }, +]; + +function schoolTypeFromInst(inst) { + if (!inst) return ''; + if (inst.pdp_id) return 'pdp'; + if (inst.edvise_id) return 'edvise'; + if (inst.legacy_id) return 'legacy'; + return ''; +} + export default function EditInst() { + const { inst_id: pageInstId } = usePage().props; + const [error, setError] = useState(null); const [addUserCounter, setAddUserCounter] = useState(1); - const [schemas] = useState([ - { name: 'Custom', selected: false }, - { name: 'PDP', selected: false }, - { name: 'Edvise', selected: false }, - { name: 'Legacy', selected: false }, - ]); + const [inst, setInst] = useState(null); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + const [schoolType, setSchoolType] = useState(''); + const [formGeneration, setFormGeneration] = useState(0); - const removeItem = (itemId) => { - const emailItem = document.getElementById(itemId); - if (emailItem) { - emailItem.remove(); + const loadInstitution = useCallback(() => { + if (!pageInstId) { + setLoadError('Set an institution before editing.'); + setLoading(false); + setInst(null); + return; } - }; + setLoading(true); + setLoadError(null); + return axios + .get('/current-institution-api') + .then(res => { + setInst(res.data); + setSchoolType(schoolTypeFromInst(res.data)); + setError(null); + }) + .catch(err => { + setInst(null); + setLoadError( + err.response?.data?.error ?? + err.message ?? + 'Failed to load institution.', + ); + }) + .finally(() => setLoading(false)); + }, [pageInstId]); + + useEffect(() => { + loadInstitution(); + }, [loadInstitution]); const incrementCounter = () => { setAddUserCounter(prev => prev + 1); @@ -37,10 +76,9 @@ export default function EditInst() { const resetForm = () => { setAddUserCounter(1); setError(null); - schemas.forEach(schema => schema.selected = false); - const form = document.getElementById('edit-institution-form'); - if (form) { - form.reset(); + setFormGeneration(g => g + 1); + if (inst) { + setSchoolType(schoolTypeFromInst(inst)); } }; @@ -50,7 +88,6 @@ export default function EditInst() { ).slice(1); return (
- {/* Default first row */}
- {/* Additional rows */} {arrOfAllAddedEmailSlots.map(id => (
-
))} @@ -119,34 +149,78 @@ export default function EditInst() { ); }; - const handleSubmit = async (event) => { + const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(event.target); - const pdp = formData.get('PDP') === 'on'; - const edvise = formData.get('Edvise') === 'on'; - const legacy = formData.get('Legacy') === 'on'; - const schoolTypeCount = [pdp, edvise, legacy].filter(Boolean).length; - if (schoolTypeCount > 1) { - setError('Select at most one of PDP, Edvise, or Legacy.'); + const type = schoolType || formData.get('school_type'); + if (!type) { + setError('Select exactly one of PDP, Edvise, or Legacy.'); return; } - // API only updates fields that are sent; to clear a school type we must send null explicitly. - // Omit edvise_id/legacy_id when that type is selected (no input to set; preserve existing). + if (type === 'pdp') { + const pid = (formData.get('pdp_id') || '').trim(); + if (!pid) { + setError('PDP Institution ID is required when PDP is selected.'); + return; + } + } + + var emailDict = {}; + var accessDict = {}; + Array.from(event.target.elements).forEach(input => { + if (input.name.endsWith('-access') || input.name.endsWith('-email')) { + let idx = Array.from(input.name)[0]; + if (input.name.endsWith('-access')) { + accessDict[idx] = input.value; + } else { + emailDict[idx] = input.value; + } + } + }); + var constructedEmailDict = {}; + for (const [key, value] of Object.entries(emailDict)) { + if (value != null && value != '') { + constructedEmailDict[value] = accessDict[key]; + } + } + const payload = { name: formData.get('inst_name'), - state: formData.get('state'), - allowed_schemas: formData.get('Custom') ? ['UNKNOWN'] : null, - allowed_emails: constructEmailDict(formData), - is_pdp: pdp, - pdp_id: pdp ? (formData.get('pdp_id') || null) : null, - edvise_id: edvise ? undefined : null, // omit when Edvise so API keeps existing; null clears - legacy_id: legacy ? undefined : null, // omit when Legacy so API keeps existing; null clears + state: formData.get('state') || null, + allowed_emails: + Object.keys(constructedEmailDict).length === 0 + ? null + : constructedEmailDict, }; - if (edvise) payload.is_edvise = true; - if (legacy) payload.is_legacy = true; + + if (type === 'pdp') { + payload.is_pdp = true; + payload.pdp_id = (formData.get('pdp_id') || '').trim(); + payload.edvise_id = null; + payload.legacy_id = null; + } else if (type === 'edvise') { + payload.is_edvise = true; + payload.pdp_id = null; + payload.legacy_id = null; + const eid = (formData.get('edvise_id') || '').trim(); + if (eid) { + payload.edvise_id = eid; + } + } else if (type === 'legacy') { + payload.is_legacy = true; + payload.pdp_id = null; + payload.edvise_id = null; + const lid = (formData.get('legacy_id') || '').trim(); + if (lid) { + payload.legacy_id = lid; + } + } + try { await axios.post('/edit-inst-api', payload); setError(null); + await loadInstitution(); + setFormGeneration(g => g + 1); } catch (err) { setError(err.response?.data?.error || err.message); } @@ -169,153 +243,212 @@ export default function EditInst() { } majorTitle="Admin Actions" minorTitle="Edit Institution [Do not use: Work in progress]" - > -
-
-
-
- - -
-
- -
- +
+
+ +
+ +
-
-
-
-
- - Schemas accepted by this institution - -
- {schemas.map((schem, idx) => ( -
-
+
+
+
+ + Institution type + +

+ Choose exactly one. +

+
+ {SCHOOL_TYPES.map(({ value, label }) => ( +
setSchoolType(value)} + className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600" />
-
-
- - - - -
-
-
- ))} -
-
+ ))} +
+ +
+
+ {schoolType === 'pdp' && ( +
+ + +

+ For PDP schools, please add the PDP_INST id of the + institution. Include any leading zeroes. +

+
+ )} + {schoolType === 'edvise' && ( +
+ + +

+ Leave blank to keep the current Edvise id, or set when + switching to Edvise to choose a specific id. +

+
+ )} + {schoolType === 'legacy' && ( +
+ + +

+ Leave blank to keep the current Legacy id, or set when + switching to Legacy to choose a specific id. +

+
+ )} + {schoolType === '' && ( +

+ Select a type to edit identifiers. +

+ )} +
-
- - -

- For PDP schools, please add the PDP_INST id of the - institution. Include any leading zeroes. -

+
+ {renderFullEmailList()}
-
- {renderFullEmailList()} +
+
-
-
- -
-
- - -
-
+
+ + +
+ + )} {error && ( -
- {error} +
+ {typeof error === 'string' ? error : JSON.stringify(error)}
)}
diff --git a/routes/web.php b/routes/web.php index a7badec..7d9bf4b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -156,6 +156,7 @@ Route::post('/edit-inst-api', [ApiController::class, 'EditInstApi']); Route::post('/add-dk-api', [ApiController::class, 'addDatakinderApi']); Route::get('/view-all-institutions-api', [ApiController::class, 'viewAllInstitutions']); + Route::get('/current-institution-api', [ApiController::class, 'getCurrentInstitutionDetails']); Route::get('/create-inst', function () { return Inertia::render('CreateInst');