momente şi schiţe de informatică şi matematică
anti point—and—click

Distribuţia pe zile a orelor dintr-o şcoală cu un singur schimb (III)

R | jQuery | orar şcolar | widget
2021 feb

[1] Distribuţia pe zile a orelor dintr-o şcoală cu un singur schimb (I..II)

[2] Reducerea numărului de ferestre pe orarul şcolar al unei zile

[3] //docerp.ro/recast (widget jQuery pentru redistribuirea interactivă a unui set de ore)

În [1] am produs o distribuţie pe zilele de lucru acceptabilă, a orelor dintr-o şcoală (cu un singur schimb) şi am desprins din ea un subset de ore care ar trebui re-distribuite, astfel încât să evităm pe cât se poate, situaţiile neconvenabile (de exemplu, cazul în care un profesor cu puţine ore, ar avea câte o oră pe zi). Aici înfiinţăm o aplicaţie – widget jQuery, inspirat din [2] – prin care să redistribuim interactiv un set dat de ore.

Redistribuirea interactivă a unui set de ore – "recast"

Primul lucru de imaginat, când te apuci de o aplicaţie interactivă este „interfaţa grafică”, prin care să pui la dispoziţia utilizatorului acţiunile necesare; în cazul de faţă (în forma finală totuşi, redată aici parţial şi numai ca imagine), interfaţa grafică pe care urmează să o explicităm arată aşa:

Datele conţinute sunt cele din tabelul produs prin R în [1] (la sfârşit).

Presupunem fireşte, obişnuinţa de a folosi butoanele şi link-urile; le-am prevăzut câte un atribut "title", încât poziţionând mouse-ul deasupra, se va produce un "tooltip" simplu care explică scurt efectul acţionării prin click a elementului respectiv; în plus, elementele de interacţiune (inclusiv, numele de profesori şi de clase) au proprietatea CSS "cursor: pointer" (sugerând că au asociate anumite acţiuni).

În pagina HTML din care se lansează aplicaţia, prevedem un element <textarea> în care utilizatorul poate „pasta” distribuţia iniţială a orelor; "Load" va încărca datele respective (şi va iniţia aplicaţia, înfiinţând anumite handlere de click), iar ulterior – va reîncărca în tabel distribuţia iniţială (ignorând modificările efectuate între timp).

"Mark" va restrânge tabelul la datele corespunzătoare clasei tastate în <input>-ul din stânga butonului (şi va şterge conţinutul acestui <input> – încât un nou click pe "Mark" va întregi înapoi tabelul de date):

Aici am selectat datele clasei 10D; vedem că 10D poate fi mutată (acţionând "SWAP", omis pe imaginea redată aici) numai între zilele Ma şi Mi (deci "SWAP" va trebui să interzică mutarea unei clase în alte zile decât cele care îi sunt alocate prin distribuţia iniţială).
Clasele au putut fi marcate individual (şi pot fi „click-ate”) fiindcă au fost ambalate în câte un element <div> (pe care am montat un anumit handler de click); fiecare element <td> al tabelului conţine zero, unul sau mai multe elemente <div> (cu câte un nume de clasă), aliniate orizontal spre stânga (prin CSS float: left).

În [2], unde era vizat orarul unei singure zile, lucrurile erau mai simple: fiecare clasă era încastrată direct în câte un <td>; acum, fiecare zi este un <td>, conţinând ca <div>-uri clasele repartizate câte unui profesor în acea zi.

"SWAP" finalizează operaţia de mutare a unei clase dintr-o zi în alta, la un profesor. De exemplu, să observăm în tabelul redat mai sus că P56 are o distribuţie defectuoasă (a vedea eventual, [1]) a orelor, (2,4,5,2,0); am putea muta o oră din ziua Mi în ziua Lu, operând pe linia lui P56 astfel: click în coloana Mi pe clasa 5C; apoi, click în coloana Lu (în interiorul casetei, dar în afara claselor existente deja); apoi, click SWAP – după care, în coloana Lu va fi adăugată şi clasa 5C, iar aceasta va fi eliminată din coloana Mi (şi a rezultat o distribuţie (3,4,4,2,0), ceva mai echilibrată decât cea iniţială).
Dar acum, 5C are cu o oră mai mult Lu şi cu o oră mai puţin, Mi (şi este obligatoriu să păstrăm numărul de ore pe zi, la fiecare clasă); deci trebuie să mutăm un 5C şi invers, din ziua Lu în ziua Mi – de exemplu, pe linia lui P61 aplicăm click pe 5C în ziua Lu şi apoi, click în coloana Mi şi în sfârşit, click SWAP (se îmbunătăţeşte puţin şi distribuţia lui P61, având acum două ore în loc de una singură, în ziua Mi).

Mutările efectuate prin SWAP sunt înregistrate intern într-o anumită structură de date, încât se poate reveni ulterior asupra acestora: click "Undo" va anula câte o operaţie SWAP, începând cu ultima efectuată.

În orice moment, click "Verify" va alerta acele distribuţii de ore pe zi la clase care diferă de cele iniţiale (încât ne putem da seama ce clase ar trebui mutate, din ce zi şi în care zi, pentru a menţine condiţiile iniţiale privitoare la numărul de ore pe zi la clase).

În sfârşit, click "Export" extrage distribuţia curentă din tabelul HTML şi produce un fişier CSV care poate fi descărcat ca atare (pentru a integra noua distribuţie a setului de ore iniţial, în distribuţia existentă a tuturor orelor şcolii; v. [1]).

Iar click pe numele unui profesor va restrânge tabelul HTML, vizualizând doar liniile acelor profesori care au clase în comun cu cel punctat prin click (util pentru a decide între care profesori să schimbăm anumite ore la o clasă sau alta); apoi, click pe "prof" (în antetul tabelului) va reconstitui întregul tabel şi va demarca acele clase care eventual, fuseseră marcate (scoţând din încurcătură şi în situaţia în care s-a demarat greşit o acţiune SWAP şi se doreşte demarcarea clasei respective, pentru a relua SWAP cu o altă clasă).

Operaţiile principale ale aplicaţiei /recast

Abandonăm ideea iniţială, de a prezenta „pas cu pas” dezvoltarea aplicaţiei schiţate mai sus; am mai abordat anterior pe aici, aplicaţii similare (inclusiv, [2]) şi în plus, aplicaţia respectivă se poate vedea deja la /recast, iar fişierele constituente sunt postate (e drept, cam la repezeală) şi pe github.

Să lămurim totuşi, în termeni simpli, operaţiile esenţiale – SWAP şi Undo.

Cum am ilustrat mai sus, la click pe numele unei clase, elementul <div> care conţine acel nume – să-l referim generic prin dCls – este marcat cu o anumită culoare; pentru a muta dCls într-o altă zi, trebuie acţionat click pe elementul <td> asociat zilei-destinaţie (pe aceeaşi linie cu dCls); <td>-ul destinaţie va fi şi el marcat cu o anumită culoare (exceptând cazul când clasa respectivă nu figurează în acea zi, în setul de ore iniţial). În final, click pe SWAP produce mutarea diviziunii dCls în <td>-ul destinaţie – ceea ce într-un „pseudocod” expeditiv s-ar descrie astfel:

La_click_SWAP {
    dCls <-- DIV-ul marcat (dacă a fost marcat unul; altfel - exit)
    td1 <-- TD-ul care conţine 'dCls'
    td2 <-- TD-ul destinaţie (dacă s-a marcat unul)
    td2.append(dCls)  /* adaugă 'dCls' după DIV-urile existente în 'td2' */
    şterge 'dCls' din 'td1'
    şterge toate marcajele (pe DIV, pe TD)
    adaugă în HISTORY indecşii de [TR, td1, td2]  /* TR=rândul celor două 'td'-uri */ 
}

În vederea operaţiei inverse Undo, menţinem o listă a mutărilor efectuate ("HISTORY", în textul de mai sus); dar înregistrăm numai indecşii de rând şi de <td>-uri – nu este necesar să înregistrăm şi clasa mutată, fiindcă aceasta este adăugată în <td>-ul destinaţie la sfârşit. Undo s-ar descrie cam aşa:

La_click_Undo {
    dacă(HISTORY este nevid) {
        last <-- ultimul tablou [id_TR, id_TD1, id_TD2] din HISTORY 
        tr <-- rândul TR de index 'id_TR', al tabelului HTML
        td1 <-- TD-ul din 'tr' de index 'id_TD1'
        td2 <-- TD-ul din 'tr' de index 'id_TD2'
        marchează   /* în culorile vizate în SWAP */ 
            ultimul DIV din 'td2', precum şi 'td1'
        declanşează 'La_click_SWAP'  /* clasa marcată în 'td2' este mutată în 'td1' */
        şterge din HISTORY tabloul [id_tr, id_td2, id_td1], tocmai adăugat de SWAP
    } 
}

Folosind cele două operaţii descrise mai sus, reuşim la un moment dat o redistribuire a orelor mai bună decât cea furnizată iniţial; link-ul Export extrage din tabelul HTML distribuţia curentă a orelor şi formulează un fişier CSV corespunzător acesteia (cu acelaşi format ca şi fişierul CSV furnizat iniţial aplicaţiei), oferindu-l spre descărcare:

bar.find('a').on('click', function(event) {
    let gridS = [['prof', 'Lu', 'Ma', 'Mi', 'Jo', 'Vi']];
    got.find('tr:gt(0)').each(function() {
        let grd = [];
        $(this).children().each(function(i, el) {
            let qls = $(el).text();
            if(i > 0) {
                qls = qls.match(/\d*[A-Z]/g);
                if(qls) qls = qls.join(" ");
            }
            grd.push(qls);
        });
        gridS.push('\n', grd.join(','));
    });  // alert(gridS);
    $(event.target).prop({
        'href': 'data:application/csv;charset=utf-8,' +  // gridS.join(""),
                encodeURIComponent(gridS.join("")),
        'download': 're_distr.csv'
    });
});

'bar' referă (ca obiect jQuery) bara de interacţiuni (care include şi link-ul 'Export') din dreapta tabelului HTML (iar 'got' este o referinţă la acest tabel); se parcurg prin .each() elementele <tr> (sărind peste primul – cel de antet al tabelului) şi pentru fiecare dintre acestea se parcurg (deasemenea prin .each()) elementele <td> componente, separând prin match(/\d*[A-Z]/g) clasele conţinute.
encodeURIComponent() a fost necesar în final, pentru a reda corect spaţiile (înscrise între clase prin .join(" ")) şi separatorul '\n' implicat între liniile care conţin fiecare, clasele dintr-un acelaşi <tr>.

Urmează să evidenţiem fişierul CSV rezultat astfel, constatând cu cât am reuşit să îmbunătăţim distribuţia de ore iniţială şi să vedem cum integrăm noua distribuţie în distribuţia globală a orelor şcolii (în locul celeia tocmai modificate prin /recast).

vezi Cărţile mele (de programare)

docerpro | Prev | Next