
Lumea BookTruck – catalog, stoc, împrumuturi și încărcări din teren pentru o bibliotecă mobilă
lumea.booktruck.ro ↗Context
BookTruck este biblioteca mobilă a asociației Cărțile pe Față, căreia îi întrețin pro-bono infrastructura IT de câțiva ani buni. Mașinile duc cărți copiilor din școlile din mediul rural, așa că în spatele fiecărei ture există un stoc de urmărit, împrumuturi de notat, restituiri de recuperat – și foarte multe poze și filmări.
Lumea BookTruck este aplicația care le ține pe toate laolaltă. Partea publică e un catalog în care oricine poate căuta cărțile care circulă cu BookTruck-urile. Panoul de administrare e locul în care administratorii, bibliotecarii și voluntarii își fac treaba de zi cu zi, de la birou sau de pe telefon, în curtea școlii.
Problema
Erau două lucruri împrăștiate. Catalogul, stocul și împrumuturile către școli stăteau parțial în tabele și parțial în memoria oamenilor: nu era clar câte exemplare dintr-o carte sunt în mașină și câte la sediu, nici câte au rămas la o anumită școală. Iar pozele și filmările de la fiecare tură ajungeau, cum spune chiar asociația, „haotic pe grupul de WhatsApp” – comprimate, nesortate și imposibil de găsit după o lună, deși organizația avea deja spațiu din belșug în Google Drive.
Ce am construit
- Un catalog public. Căutarea găsește cărțile după titlu, autor sau ISBN, cu sau fără diacritice, iar caseta de căutare arată câte cărți, autori și genuri are biblioteca. Prima pagină mai are ultimele cărți adăugate, cele mai împrumutate cărți din toate timpurile și un top 10 al lunii trecute. Fiecare carte are pagina ei, cu copertă, autori, ISBN, limbă, număr de pagini și vârsta recomandată.
- Catalogul din panou. Cărți, autori, genuri și școli, cu tabele care se pot căuta, sorta și pagina și în care alegi ce coloane vezi. Stocul unei cărți e împărțit pe locații, iar listele de împrumuturi și restituiri se pot exporta în CSV.
- Adăugare rapidă de cărți. Codul de bare ISBN se scanează cu camera telefonului, direct din browser. Scannerul caută doar formatele tipărite pe cărți, iar pe iPhone-urile cu mai multe camere alege camera din spate care poate focaliza de aproape și ține minte alegerea. La un ISBN nou, titlul, autorul, numărul de pagini, limba și coperta se completează singure din Goodreads. Dacă ISBN-ul există deja, panoul te avertizează. Coperțile sunt redimensionate și convertite în browser înainte de încărcare și primesc un placeholder blurhash.
- Împrumuturi și restituiri dintr-o singură fereastră. Butonul „Împrumut / Restituire” deschide o fereastră în care scanezi sau tastezi ISBN-ul, alegi locația, școala și cantitatea și confirmi. Fereastra ține minte ultimele alegeri, pentru că o vizită la o școală le folosește pe aceleași, iar după fiecare carte e gata pentru următoarea. La restituire vezi câte exemplare din cartea respectivă mai are școala. O restituire se împarte automat pe cele mai vechi împrumuturi deschise și poate ajunge în altă locație decât cea din care a plecat cartea, iar stocul se mută odată cu ea. Cantitățile sunt verificate pe server față de stocul disponibil și față de ce a rămas efectiv împrumutat.
- Schimbări de stare. Bibliotecarii pot marca exemplare ca donate sau reciclate, cu o notă. Acestea ies din stoc, dar rămân în istoricul cărții.
- Locații. BookTruck-urile, sediul și celelalte locuri în care stau cărțile sunt înregistrări gestionate de administratori, cu ordine și culoare proprie în grafice. O locație scoasă din uz se retrage, nu se șterge, ca istoricul să rămână lizibil. Tot stocul unei locații se poate muta în alta, în loturi de câte 20 de cărți, cu bară de progres. Dacă mutarea se întrerupe, poate fi reluată, iar fiecare carte mutată primește o intrare în istoric.
- Istoricul fiecărei cărți. Adăugarea, editările, modificările de stoc, schimbările de stare, împrumuturile și restituirile apar într-o singură listă, iar administratorii o pot descărca în CSV.
- Panoul de control. Cărți noi, împrumuturi și restituiri din ultimele 30 de zile comparate cu cele 30 dinainte, un grafic cu împrumuturile și restituirile pe ultimele 90 de zile, distribuția cărților pe locații, topul împrumuturilor, statistici generale, membrii echipei, un rezumat al încărcărilor de la voluntari și ultimele 100 de tranzacții.
- Sesiuni de încărcare pentru voluntari. Un administrator creează o sesiune cu o etichetă și o dată de expirare, implicit a doua zi la aceeași oră. Asta creează automat un subdosar în Shared Drive-ul asociației și un link lung, aleatoriu, care se copiază dintr-un clic. Voluntarii deschid linkul de pe telefon, fără cont, și își scriu opțional numele, pe care pagina îl ține minte. Pot alege fișiere sau trage peste pagină fișiere ori dosare întregi. Pagina acceptă poze și clipuri, inclusiv HEIC și MOV de pe iPhone, și respinge cu explicație fișierele nepotrivite, goale sau deja adăugate. Fiecare fișier are propria bară de progres, cu mesaje care se schimbă pe parcurs („BookTruck-ul rulează cu pozele tale…”). Cele eșuate se pot reîncerca individual sau toate odată. Un cronometru vizibil permanent arată cât mai e deschisă sesiunea și trece pagina în starea „expirată” fără reîncărcare. Cât timp încărcarea e în curs, ecranul nu se stinge, iar închiderea paginii cere confirmare. În panou, fiecare sesiune are numărul de fișiere, dimensiunea totală, viteza medie și lista fișierelor și poate fi închisă sau prelungită oricând.
- Echipa. Administratorii trimit invitații pe e-mail cu un rol stabilit dinainte. O invitație expiră, poate fi retrimisă, caz în care cea veche se anulează, sau revocată. Cine o acceptă își completează profilul și intră direct în panou. Administratorii mai pot schimba roluri, pot bloca un cont temporar sau permanent, cu motiv, și pot prelua temporar contul altcuiva ca să reproducă o problemă, cu un banner vizibil cât timp o fac. Un jurnal de activitate păstrează invitațiile, schimbările de rol, editările de profil, blocările, autentificările și preluările de cont. Avatarele au un punct verde pentru cine e online, iar când intră un coleg în panou apare o notificare.
- Contul propriu și tema. Fiecare își editează numele, telefonul și avatarul și alege între tema luminoasă, cea întunecată sau cea a sistemului.
Permisiunile sunt o matrice pe resurse (cărți, stoc, împrumuturi, restituiri, autori, genuri, școli, locații, statistici, echipă, galerie) pentru trei roluri. Voluntarii consultă catalogul și înregistrează împrumuturi și restituiri. Bibliotecarii gestionează tot catalogul, inclusiv schimbările de stare. Administratorii se ocupă în plus de școli, locații și mutări de stoc, de exportul istoricului, de sesiunile de încărcare și de echipă. Fiecare acțiune e verificată pe server, iar meniul ascunde secțiunile pe care rolul tău nu le poate folosi.
Sub capotă
Decizia interesantă ține de încărcări: fișierele nu trec niciodată prin server. Serverul doar deschide în Google Drive o sesiune de resumable upload cu un cont de serviciu și îi dă browserului adresa ei. Browserul trimite apoi fișierul în bucăți de 8 MB, cu maximum trei fișiere în paralel, ca să nu sufoce conexiunea telefonului. Dacă legătura cade sau se blochează, încărcarea nu o ia de la capăt: browserul așteaptă din ce în ce mai mult între încercări, întreabă Drive-ul cât a primit și continuă de acolo. Orice progres resetează numărul de reîncercări permise, așa că un clip lung trimis pe o conexiune instabilă merge mai departe cât timp avansează. Dacă fișierele ar fi trecut prin server, ne loveam de limita de 4,5 MB pe cerere și de timeout-urile din Vercel Functions. Voluntarii nu primesc niciodată acces la Drive, fiecare sesiune are un plafon de spațiu, iar fiecare fișier încheiat se înregistrează (nume, tip, dimensiune, durată) și alimentează statisticile din panou.
Stocul e ținut pe perechi carte–locație, cu două numere: câte exemplare are locația și câte sunt disponibile acum. Un împrumut scade doar disponibilul, iar o restituire în altă locație mută exemplarele dintr-un stoc în altul. Locațiile sunt referite printr-un identificator care nu se schimbă niciodată, așa că împrumuturile și jurnalele vechi rămân corecte și după ce o locație e redenumită sau retrasă. Calculul pentru mutarea unei locații întregi e o funcție pură, testată separat de baza de date.
Autentificarea a trecut de la Supabase Auth la Better Auth, cu magic link fără parolă și fără înregistrare liberă. Am păstrat toate ID-urile de utilizator și cheile străine existente printr-un backfill aditiv, așa că nu a trebuit remapat nimic. Trimiterea linkurilor e limitată în Redis, pe adresă de e-mail și pe IP, pentru că fiecare e-mail costă credite. Rolul de administrator nu poate fi acordat, schimbat sau preluat din aplicație, ci doar din baza de date. Supabase a rămas doar pentru stocarea coperților și a avatarelor, cu scrieri făcute exclusiv pe server, după verificarea permisiunilor.
Redis mai ține în cache statisticile catalogului, reîmprospătate săptămânal, și topul lunii trecute, generat la prima vizită din luna nouă, fără cron. Tot el poartă semnalele de prezență: câte unul la două minute, ca o echipă mică să rămână lejer în limitele unui plan gratuit. Dacă Redis lipsește, funcțiile care depind de el sunt sărite, iar restul aplicației merge mai departe. Modificările de schemă sunt migrări SQL scrise de mână, iar toate datele calendaristice sunt fixate pe ora României, ca serverul și browserul să afișeze mereu aceeași oră. Regulile de bază – permisiuni, mutări de stoc, invitații, jurnale, exportul istoricului și validarea formularelor de împrumut și restituire – sunt acoperite de teste Vitest.
Rezultat
Un singur link pentru fiecare tură, iar fișierele ajung ordonate în Drive, nu pe un grup de chat. Filmările mari se încarcă de pe telefon chiar și pe conexiuni slabe, fără să atingă – sau să sufoce – serverul. Un împrumut sau o restituire la o școală înseamnă un ISBN scanat și o confirmare, iar stocul pe fiecare mașină și locație e la zi. Bibliotecarii și voluntarii lucrează în același panou, fiecare văzând doar ce îi permite rolul, iar catalogul din care împrumută copiii poate fi, în sfârșit, căutat de oricine.
Tehnologii
Next.js 16 (App Router), React 19, TypeScript, Prisma 7 pe Supabase Postgres, Better Auth, Tailwind CSS 4 cu shadcn/ui, TanStack Table, react-hook-form + zod, Recharts, ZXing, Redis, Google Drive API, Brevo, Supabase Storage, Vitest, Vercel.
Tehnologii:
- Next.js
- Prisma
- Better Auth
- Google Drive API
- Supabase
- Redis
- Vitest
- Vercel
Partajează pe: