momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

O aplicaţie de salarizare

JSON | jQuery | limbajul R | salarizare
2018 jan

Revedem o aplicaţie de salarizare pentru învăţământul preuniversitar. Vizăm ceva mai general decât "fluturaşul" individual (v. Calculator salarii): listăm salariile după cele maximum 23 de tupluri ('vechime', 'gradaţie') valabile, pentru fiecare specificaţie valabilă "Funcţie/Grad/Studii" - luând eventual în calcul numai câteva (cele mai obişnuite) dintre sporurile posibile (v. Salarii 2017 versus 2018).

Culegerea datelor din "Monitorul Oficial" într-un fişier CSV

Găsim reglementările necesare în "Monitorul Oficial" - de exemplu:

monitor2016.png

Vom structura datele plecând de la aceste trei atribute:

  • 'Funcţie': Profesor, Institutor, Învăţător / Educator / Maistru instructor, "fără specialitate"
  • 'Grad': I, II, Definitiv, Debutant, "fără"
  • 'Studii': S ("superioare de lungă durată"), SSD, Medii

Avem deja 4×5×3 = 60 combinaţii de valori, dar unele dintre acestea trebuie excluse - de exemplu, nu avem "Profesor - Definitiv - Medii", nici "Învăţător - I - S". Pentru acelea dintre aceste combinaţii care sunt valabile, salariul depinde de "vechimea în învăţământ" şi de gradaţia de "vechime în muncă" (cum se vede în imaginea de mai sus), iar salariul final (atribuit prin "statul de plată") va depinde şi de anumite sporuri individuale reglementate în cadrul legii respective.

Trebuie să preluăm din "Monitorul Oficial", cifrele respective… Dar "informatizarea" instituită la noi vizează prezentarea pe hârtie ("în format A4"), nu prelucrarea datelor; este greu de făcut un program prin care să depistezi şi să extragi dintr-un fişier PDF acele date numerice de care ai nevoie - cea mai simplă soluţie rămâne aceea manuală.

În cazul de faţă am procedat aşa: am creat întâi un fişier CSV "grila.csv" conţinând:

func,grad,stud,vech,tran,Baza
Profesor,I,S,peste 40,5,

Prin Copy&Paste am multiplicat a doua linie de 23 de ori (câte valori avem în coloanele "Gradaţia" -v. imaginea redată mai sus) şi am înlocuit sintagma "peste 40,5" în liniile respective, corespunzător vechimii şi gradaţiilor pentru "Profesor - I - S":

func,grad,stud,vech,tran,Baza
Profesor,I,S,peste 40,5,
Profesor,I,S,35-40,5,
Profesor,I,S,30-35,5,
Profesor,I,S,25-30,5,
Profesor,I,S,22-25,5,
Profesor,I,S,18-22,4,
Profesor,I,S,18-22,5,
Profesor,I,S,14-18,3,
Profesor,I,S,14-18,4,
Profesor,I,S,14-18,5,
Profesor,I,S,10-14,3,
Profesor,I,S,10-14,4,
Profesor,I,S,10-14,5,
Profesor,I,S,6-10,2,
Profesor,I,S,6-10,3,
Profesor,I,S,6-10,4,
Profesor,I,S,6-10,5,
Profesor,I,S,1-6,0,
Profesor,I,S,1-6,1,
Profesor,I,S,1-6,2,
Profesor,I,S,1-6,3,
Profesor,I,S,1-6,4,
Profesor,I,S,1-6,5,

Apoi am multiplicat blocul de text obţinut (exceptând antetul din prima linie), înlocuind "I" cu "II", apoi cu "DEF", etc., sau înlocuind "S" cu "SSD" etc. - rezultând un fişier CSV conţinând exact 380 de linii (plus antetul), corespunzând împreună datelor din imaginea obţinută de la "Monitorul Oficial"; apoi, am adus într-o fereastră alăturată documentul PDF respectiv şi - cu maximă atenţie - am tastat în "grila.csv" (pe "coloana" denumită 'Baza') cifrele citite din PDF.

Este drept că mi-a luat trei ore, până să fiu sigur că nu am greşit vreo cifră - mai făcând faţă şi reacţiei fireşti de a abandona totul; având nevoie în diverse prelucrări, de seturi de date publice postate de către instituţiile noastre - ajungi să înţelegi că reacţia de abandonare este şi cea scontată, de către diriguitorii lucrurilor (dar mai poate fi vorba şi de altceva; e suficient să ştii Microsoft Word ca să ajungi "înalt funcţionar public").

Internalizarea datelor din fişierul CSV

În R obţinem imediat un obiect de tip 'data.frame', din fişierul "grila.csv":

vb@Home:~/16_sal$  R  -q
> grila <- read.csv("grila.csv")
> str(grila)
'data.frame':	380 obs. of  6 variables:
 $ func: Factor w/ 4 levels "fărăSpec","Institutor",..: 4 4 4 4 4 4 4 4 4 4 ...
 $ grad: Factor w/ 5 levels "","DBT","DEF",..: 4 4 4 4 4 4 4 4 4 4 ...
 $ stud: Factor w/ 3 levels "M","S","SSD": 2 2 2 2 2 2 2 2 2 2 ...
 $ vech: Factor w/ 11 levels "10-14","14-18",..: 10 8 7 6 5 4 4 2 2 2 ...
 $ tran: int  5 5 5 5 5 4 5 3 4 5 ...
 $ Baza: num  3948 3779 3613 3497 3365 ...
> head(grila, 3)
      func grad stud     vech tran Baza
1 Profesor    I    S peste 40    5 3948
2 Profesor    I    S    35-40    5 3779
3 Profesor    I    S    30-35    5 3613

R ar fi cel mai potrivit de utilizat, pentru o aplicaţie flexibilă de salarizare (acoperind toate categoriile de bugetari); desigur, interpretorul de R ar fi rezident pe server, iar "interfaţa cu utilizatorul" (pentru a solicita şi a lua în calcul sporurile individuale) ar necesita până la urmă, instrumente specifice browser-ului. Scopul nostru aici este mai modest (nu vizăm nivelul generic) şi nu vrem să folosim resurse de pe server, ci numai ceea ce ne poate oferi un browser.

Să transformăm 'grila' într-un obiect JSON, uşor de încorporat apoi în javaScript:

> library(jsonlite)
> grila_js <- toJSON(grila)
> write(paste0("var grila_16 = '", grila_js, "';"), file="grila.js")

Fişierul "grila.js" obţinut astfel defineşte o variabilă javaScript care conţine un şir de caractere:

var grila_16 = '[{"func":"Profesor","grad":"I","stud":"S","vech":"peste 40","tran":5,
"Baza":3948}, // ...
{"func":"fărăSpec","grad":"","stud":"M","vech":"sub 1","tran":5,"Baza":1987}]';

Prin JSON.parse() vom putea extrage din acest şir cele 380 de obiecte javaScript - cum arată următorul mic experiment (folosind interpretorul nodejs):

// adăugăm la sfârşitul fişierului 'grila.js':
console.log(grila_16.length);
var grila = JSON.parse(grila_16);
console.log(grila.length);
console.log(grila[0]);
vb@Home:~/16_sal$ nodejs grila.js 
30478  # lungimea şirului 'grila_16'
380  # 380 obiecte JSON, precum acesta:
{ func: 'Profesor',
  grad: 'I',
  stud: 'S',
  vech: 'peste 40',
  tran: 5,
  Baza: 3948 }

Desigur, era de scurtat şirul 'grila_16' - preferând "Prof" în loc de "Profesor", "40.." în loc de "peste 40", etc. Trebuie să mai precizăm că salariile au fost modificate de mai multe ori în cursul anului (cu republicare în "Monitorul Oficial", cam în acelaşi format) şi în loc să actualizez fişierul iniţial "grila.csv", a fost mai uşor să operez în R pe structura 'grila' (folosind comanda 'edit(grila)').

Cadrul HTML şi cadrul operaţional al aplicaţiei

Creem un fişier "salar.html" conţinând trei elemente <select> (pentru 'Funcţie', 'Grad' şi respectiv, 'Studii') într-o diviziune identificată prin 'fgs', precum şi două diviziuni alăturate 'sal1' şi 'sal2', menite să tabeleze obiecte JSON din 'grila' (corespunzător opţiunilor selectate), respectiv să redea "statul de plată" constituit pentru unul dintre acestea:

salar17.png

Mai prevedem un element <script> prin care să importăm o bibliotecă jQuery (optăm pentru versiunea 1.11) şi unul prin care să încărcăm "grila.js". Vom adăuga handler-ele necesare interacţiunii cu listele de selecţie şi cu liniile de tabel, în cadrul unei construcţii jQuery '<script>$(function() { ... });</script>' plasate la sfârşitul fişierului "salar.html".

Pentru a obţine tabelul din diviziunea "sal1" (reprezentând obiectele JSON din 'grila', în care valorile primelor trei câmpuri sunt cele curent selectate), montăm pe diviziunea care ambalează cele trei <select>-uri următorul handler pentru evenimentul 'change':

    $('#fgs').on('change', "select", function() {
        var tmp = $('#fgs select');
        var func = tmp.eq(0).val(), 
            grad = tmp.eq(1).val(), 
            stud = tmp.eq(2).val();
        $('#sal1').html(subtable(func, grad, stud));
    });
    $('#fgs select').trigger('change');

Funcţia 'subtable(f, g, s)' invocată în acest handler, va trebui să se găsească în "grila.js"; ea extrage din variabila 'grila' obiectele JSON corespunzătoare valorilor selectate şi formulează elementul '<table>' pe care handler-ul redat mai sus îl înscrie în diviziunea 'sal1':

var grila = JSON.parse(grila_16);
var table = "<table class='saltable'>" +
            "<tr> <th>Funcţie</th> <th>Grad</th> <th>Studii</th>" +
            "<th>Vechime</th> <th>Tranşa</th> <th>Baza</th> </tr>";
function subtable(f, g, s) {
    var subset = grila.filter(function(d) { 
                       return d.func == f && d.grad == g && d.stud == s; 
                 });
    if(subset.length == 0) return "<p>Nu există!</p>";
    var t_arr = [table];
    subset.forEach(
        function(d) { 
            t_arr.push("<tr><td>", d.func, "</td><td>", d.grad, "</td><td>", d.stud,
                       "</td><td>",d.vech, "</td><td>", d.tran, "</td><td>", d.Baza, 
                       "</td></tr>");
    });
    t_arr.push("</table>");
    return t_arr.join('');
};
// console.log(subtable("Profesor", "DBT", "S"));

Desigur, decomentând ultima linie putem verifica rezultatul produs de 'subtable()' (care a fost înscris în diviziunea 'sal1' pe imaginea de mai sus, formulat acum în HTML):

vb@Home:~/16_sal$ nodejs grila.js
<table class='saltable'>
<tr><th>Funcţie</th><th>Grad</th><th>Studii</th><th>Vechime</th><th>Tranşa</th><th>Baza</th>
</tr> <tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>0</td><td>2045</td></tr>
<tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>1</td><td>2149</td></tr>
<tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>2</td><td>2234</td></tr>
<tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>3</td><td>2322</td></tr>
<tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>4</td><td>2387</td></tr>
<tr><td>Profesor</td><td>DBT</td><td>S</td><td>sub 1</td><td>5</td><td>2453</td></tr>
</table>

Astfel de testări directe sunt importante; acum vreo doi ani (când am realizat aplicaţia) m-am bazat numai pe ceea ce-mi arăta browser-ul şi abia acum - când scriind despre aplicaţie, am făcut totuşi testul redat mai sus - am sesizat că iniţial, aveam 't_arr.push(..., "</td><tr>");', uitând să închid elementul <tr>; browser-ul este permisiv - "corectează" singur asemenea greşeli şi nu te atenţionează neapărat asupra lor.

Dar mă mir acum, că nu am observat la timp inutilitatea primelor trei coloane din tabelul înscris în diviziunea 'sal1' (a vedea şi imaginea de mai sus): acestea conţin câte o aceeaşi valoare, anume valoarea din elementul <select> la fel denumit ca şi coloana respectivă. O "reparaţie" foarte simplă (ca să nu zicem "cârpeală") ar consta în atributarea elementelor <td> pentru aceste trei coloane, cu proprietatea CSS "display:none" (evitând astfel, modificarea codului existent).

Avem nevoie apoi, de un handler prin care să formulăm statul de plată corespunzător datelor de pe una sau alta dintre liniile existente în tabelul constituit în diviziunea 'sal1' (anume, acea linie care va fi punctată cu mouse-ul de către utilizator) şi să-l înscriem în diviziunea alăturată 'sal2':

    $('#sal1').on('click', "td", function() {
        var tr = $(this).parent();
        tr.siblings().removeClass('alesul');
        tr.addClass('alesul');  // marchează rândul asociat statului de plată
        var tds = $(this).parent().children();
        var par = [];  // culege cele 5 valori de pe linie marcată
        $(tds).each(function() {
            par.push($(this).text());
        });
        $('#sal2').html(salariu(par));
    });

Funcţia 'salariu()' implicată în acest handler, trebuie încorporată în "grila.js"; se determină din tabloul 'grila' obiectul JSON corespunzător tabloului (cu valorile de pe linia marcată) primit ca argument şi se constituie "statul de plată iniţial", apelând funcţia 'salini()':

function salariu(par, smd) { 
    if(smd===undefined) {
        smd = [false, false, false, 0, 0, 72];
    }
    var gob = grila.filter(function(d) { 
                  return d.func == par[0] && d.grad == par[1] && d.stud == par[2]
                         && d.vech == par[3] && d.tran == par[4];
              })[0];
    var caption = "<caption style='font-size:0.9em;color:navy;font-weight:bold'>" + 
                  par.slice(0, 5).join(' / ') + "</caption>";
    var star = salini(gob, smd).join('');
    return ["<table class='saltable1'>", caption, star, "</table>"].join('');
}

Funcţia salini(gob, smd) calculează statul de plată şi formulează elementele <tr> ale tabelului returnat de funcţia de mai sus; sunt luate în calcul şi cinci (cele mai obişnuite) dintre sporurile posibile ('smd', neatribuite iniţial), prevăzând şi elementele de formular necesare (<input>-uri, <select>) pentru ca utilizatorul să le poată preciza:

salar17_1.png

'salini()' trebuie inclusă şi ea, în "grila.js" - dar nu-i cazul să o mai redăm aici: este singura componentă a aplicaţiei (pe lângă cele 380 de salarii de bază preluate din "Monitorul Oficial") care ar depinde de regulile de salarizare curente şi de valorile prezente ale unor coeficienţi sau procente (schimbătoare după an, lună, sau ministru).

Avem nevoie de încă un handler; dacă utilizatorul va bifa "Merit", "Dirigenţie" (cum am şi făcut pentru imaginea de mai sus) etc., sau va preciza numărul de ore suplimentare, sau "Alte Sume" - atunci statul de plată trebuie recalculat şi reafişat, pentru a lua în calcul sporurile respective:

    $('#sal2').on('change', 'input, select', function(event) {
        event.preventDefault();
        var tr = $('#sal1').find('tr.alesul');
        var par = $(tr).children('td').map(function() {
            return $(this).text();
        }).get();
        var smd = [];
        $('#sal2').find('input:checkbox').each(function() {
            if($(this).is(":checked")) {
                smd.push(true);
            } else { smd.push(false); }
        });
        $('#sal2').find('input:text').each(function() {
            smd.push($(this).val());
        });
        smd.push($('#sal2').find('select').val());
        $('#sal2').html(salariu(par, smd));
    });

În Salarii 2017 versus 2018 am reuşit să completez aplicaţia descrisă mai sus, alăturând datele corespunzătoare salarizării pe 2017, cu cele pentru 2018.

vezi Cărţile mele (de programare)

docerpro | Prev | Next