Blog · November 2026 · 6 min read Blog · Novembre 2026 · 6 min de lectureالمدونة · نوفمبر 2026 · 6 دقائق قراءة
I replaced a paper attendance book with a web app J'ai remplacé un cahier de présence par une appli webعوّضت دفتر الحضور بتطبيق ويب
Not a startup, not a hackathon: a small tool for one real user at the medical imaging center I manage. It taught me more about building software than many bigger projects. Pas une startup, pas un hackathon : un petit outil pour un seul vrai utilisateur, dans le centre d'imagerie médicale que je gère. Il m'a appris plus sur la conception de logiciels que bien des projets plus gros.لا شركة ناشئة ولا هاكاثون: أداة صغيرة لمستخدم حقيقي واحد، في مركز التصوير الطبي الذي أديره. علّمتني عن تصميم البرمجيات أكثر مما علّمتني مشاريع أكبر بكثير.
The notebookLe cahierالدفتر
At the center, attendance was written in a notebook, and at the end of each month the pay was calculated by hand. That sounds simple until you look at the rules. Some staff are paid per day, depending on their role. The radiographer is paid per scan. Technicians and receptionists who also run scans get a bonus per scan. Replacement doctors are paid an amount agreed for the day. The head doctor is tracked but not paid through this system. Au centre, les présences étaient notées dans un cahier, et en fin de mois la paie était calculée à la main. Ça paraît simple, jusqu'à ce qu'on regarde les règles. Certains sont payés à la journée, selon leur rôle. Le manipulateur est payé à l'acte. Les techniciens et réceptionnistes qui font aussi des examens touchent un bonus par examen. Les médecins remplaçants sont payés un montant convenu pour la journée. Le médecin chef est suivi, mais pas payé par ce système.في المركز، كان الحضور يُسجَّل في دفتر، وفي نهاية الشهر تُحسب الرواتب يدوياً. يبدو ذلك بسيطاً، إلى أن ننظر في القواعد. بعضهم يُدفع له باليوم حسب دوره. والمُشغِّل (manipulateur) يُدفع له بالفحص. والتقنيون وموظفو الاستقبال الذين يُجرون فحوصاً أيضاً يتقاضون مكافأة عن كل فحص. والأطباء المستخلفون يُدفع لهم مبلغ متفق عليه لليوم. أما الطبيب الرئيسي فيُتابَع حضوره، لكنه لا يتقاضى أجره عبر هذا النظام.
The result: mistakes were easy, and nobody knew the month's total until the month was over. Résultat : les erreurs étaient faciles, et personne ne connaissait le total du mois avant la fin du mois.النتيجة: الأخطاء سهلة الوقوع، ولا أحد يعرف مجموع الشهر قبل نهايته.
Version 1: as small as possibleVersion 1 : le plus petit possibleالنسخة 1: أصغر ما يمكن
The first version, in August 2026, ran on one computer at the center. Data was saved in JSON files, and a small script started the app. No server, no account, no hosting. It was enough to answer the only question that mattered at first: does a calendar with live totals really replace the notebook? La première version, en août 2026, tournait sur un seul ordinateur du centre. Les données étaient enregistrées dans des fichiers JSON, et un petit script lançait l'appli. Pas de serveur, pas de compte, pas d'hébergement. C'était suffisant pour répondre à la seule question qui comptait au départ : un calendrier avec des totaux en direct remplace-t-il vraiment le cahier ?كانت النسخة الأولى، في أوت 2026، تعمل على حاسوب واحد في المركز. تُحفظ البيانات في ملفات JSON، ويشغّل سكربت صغير التطبيق. لا خادم، ولا حساب، ولا استضافة. وكان ذلك كافياً للإجابة عن السؤال الوحيد المهم في البداية: هل تعوّض رزنامة بمجاميع مباشرة الدفترَ فعلاً؟
It did. And it immediately raised the next need: entering attendance from a phone, from anywhere, not only from that one computer. Oui. Et ça a tout de suite fait apparaître le besoin suivant : saisir les présences depuis un téléphone, de n'importe où, pas seulement depuis cet ordinateur.نعم. وظهرت فوراً الحاجة التالية: تسجيل الحضور من الهاتف، ومن أي مكان، لا من ذلك الحاسوب فقط.
Turning the rules into one small functionTransformer les règles en une petite fonctionتحويل القواعد إلى دالة صغيرة
The key design decision was the data model. For each day and each employee, the app stores one value: true for "present", or a number. The number means the scans done by the radiographer, extra scans done by a technician or receptionist, or the amount paid to a replacement doctor. One value per cell, and all the complexity lives in a single, readable function:
La décision clé a été le modèle de données. Pour chaque jour et chaque employé, l'appli enregistre une seule valeur : true pour « présent », ou un nombre. Ce nombre représente les examens du manipulateur, les examens faits en plus par un technicien ou un réceptionniste, ou le montant versé à un médecin remplaçant. Une valeur par case, et toute la complexité tient dans une seule fonction lisible :كان القرار الأهم هو نموذج البيانات. لكل يوم ولكل موظف، يسجّل التطبيق قيمة واحدة: true تعني « حاضر »، أو عدداً. هذا العدد يمثّل فحوص المُشغِّل، أو الفحوص الإضافية لتقني أو لموظف استقبال، أو المبلغ المدفوع لطبيب مستخلف. قيمة واحدة لكل خانة، وكل التعقيد يجتمع في دالة واحدة سهلة القراءة:
export function dailyPay(employee, value, rates): number {
if (value === undefined) return 0 // absent
if (employee.monthlySalary) { // fixed salary: only extra scans count
return typeof value === "number" && canDoBonusScans(employee.role)
? value * rates.scanBonus : 0
}
switch (employee.role) {
case "chef": return 0 // not paid here
case "medecin": return typeof value === "number" ? value : 0
case "manipulateur": return typeof value === "number" ? value * rates.manipulateur : 0
default: // present, maybe with extra scans
return typeof value === "number" && canDoBonusScans(employee.role)
? rates[employee.role] + value * rates.scanBonus
: rates[employee.role]
}
}
The month's payslip is just this function summed over the days. Because it is recalculated on every entry, the total is always up to date. La fiche de paie du mois, c'est simplement cette fonction additionnée sur les jours. Comme elle est recalculée à chaque saisie, le total est toujours à jour.كشف رواتب الشهر هو ببساطة مجموع هذه الدالة على الأيام. وبما أنه يُعاد حسابه عند كل إدخال، يبقى المجموع محدَّثاً دائماً.
Version 2: online, on a phoneVersion 2 : en ligne, sur téléphoneالنسخة 2: على الإنترنت، وعلى الهاتف
In October, the app moved online: Next.js on Vercel, data in Upstash Redis, both on free plans. Three things mattered: En octobre, l'appli est passée en ligne : Next.js sur Vercel, données dans Upstash Redis, les deux en offre gratuite. Trois points comptaient :في أكتوبر، انتقل التطبيق إلى الإنترنت: Next.js على Vercel، والبيانات في Upstash Redis، وكلاهما بعرض مجاني. وكانت ثلاث نقاط مهمة:
- Phone first. Tab navigation, entry in panels at the bottom of the screen, large touch targets, and it installs on the home screen like an app.D'abord le téléphone. Navigation par onglets, saisie dans des panneaux en bas d'écran, grandes zones tactiles, et elle s'installe sur l'écran d'accueil comme une appli.الهاتف أولاً. تنقّل بالتبويبات، وإدخال في لوحات أسفل الشاشة، ومساحات لمس كبيرة، ويُثبَّت على الشاشة الرئيسية مثل أي تطبيق.
- No lost entries. Each day is its own field in a Redis hash, as is each employee and each rate. Saving one day only touches that day, so a save from the phone and one from the computer at the same moment cannot erase each other. The JSON-file version has the same concern: two saves at almost the same time could each read the old file, and the second write would make a just-added employee "disappear". So every read-modify-write on a file is queued behind the previous one.Aucune saisie perdue. Chaque jour est un champ à part dans un hash Redis, tout comme chaque employé et chaque tarif. Enregistrer un jour ne touche que ce jour : un enregistrement depuis le téléphone et un autre depuis l'ordinateur au même moment ne peuvent pas s'effacer. La version en fichiers JSON a le même souci : deux enregistrements presque simultanés pouvaient lire tous les deux l'ancien fichier, et la deuxième écriture faisait « disparaître » un employé qui venait d'être ajouté. Chaque lecture-modification-écriture d'un fichier attend donc la précédente.لا يضيع أي إدخال. كل يوم حقل مستقل في hash داخل Redis، وكذلك كل موظف وكل تسعيرة. حفظ يوم لا يمسّ إلا ذلك اليوم: حفظ من الهاتف وآخر من الحاسوب في اللحظة نفسها لا يمحو أحدهما الآخر. ونسخة ملفات JSON تعالج المشكلة نفسها: حفظان شبه متزامنين قد يقرآن الملف القديم معاً، فتجعل الكتابة الثانية موظفاً أُضيف للتو « يختفي ». لذا تنتظر كل عملية قراءة-تعديل-كتابة على الملف انتهاء سابقتها.
- Simple, solid access. Every page and API route goes through one check. The session cookie (HttpOnly, Secure) is derived from the password, so changing the password signs out every device at once. Every API input is validated with Zod.Un accès simple et solide. Toutes les pages et routes API passent par un seul contrôle. Le cookie de session (HttpOnly, Secure) est dérivé du mot de passe : changer le mot de passe déconnecte tous les appareils d'un coup. Chaque entrée de l'API est validée avec Zod.دخول بسيط ومتين. تمرّ كل الصفحات ومسارات الـ API عبر نقطة تحقق واحدة. وملف الجلسة (cookie من نوع HttpOnly و Secure) مشتق من كلمة المرور: تغييرها يُخرج كل الأجهزة دفعة واحدة. وكل مدخلات الـ API يُتحقَّق منها بـ Zod.
The real requirements came after launchLes vrais besoins sont arrivés après la mise en ligneالاحتياجات الحقيقية ظهرت بعد النشر
As soon as the app was used every day, new needs showed up: replacement doctors, extra-scan bonuses, a fixed monthly salary for some staff, and later a radiographer role paid per day. None of these were in the first version, and that was fine. Because the model was simple (one value per cell, one pay function), each new rule was a small change, not a rewrite. Dès que l'appli a été utilisée chaque jour, de nouveaux besoins sont apparus : les médecins remplaçants, les bonus d'examens en plus, un salaire mensuel fixe pour certains, et plus tard un rôle de manipulateur payé à la journée. Rien de tout ça n'était dans la première version, et c'était très bien. Comme le modèle était simple (une valeur par case, une fonction de paie), chaque nouvelle règle a été un petit changement, pas une réécriture.بمجرد أن صار التطبيق يُستعمل كل يوم، ظهرت احتياجات جديدة: الأطباء المستخلفون، ومكافآت الفحوص الإضافية، وراتب شهري ثابت لبعضهم، ثم لاحقاً دور مُشغِّل يُدفع له باليوم. لم يكن أي من ذلك في النسخة الأولى، وكان ذلك جيداً. وبما أن النموذج بسيط (قيمة لكل خانة، ودالة رواتب واحدة)، كانت كل قاعدة جديدة تعديلاً صغيراً، لا إعادة كتابة.
What I learnedCe que j'ai apprisما تعلّمته
- Start smaller than you think. A version that runs on one computer was enough to prove the idea.Commencer plus petit qu'on ne le pense. Une version sur un seul ordinateur suffisait à prouver l'idée.البدء أصغر مما نظن. نسخة على حاسوب واحد كانت كافية لإثبات الفكرة.
- The data model is the design. Getting "one value per day per person" right made every later feature easy.Le modèle de données, c'est la conception. Bien choisir « une valeur par jour et par personne » a rendu chaque fonctionnalité suivante facile.نموذج البيانات هو التصميم. حسن اختيار « قيمة واحدة لكل يوم ولكل شخص » جعل كل ميزة لاحقة سهلة.
- Small apps still deserve care. When the numbers are someone's salary, losing one entry is not acceptable.Une petite appli mérite quand même du soin. Quand les chiffres sont le salaire de quelqu'un, perdre une saisie n'est pas acceptable.التطبيق الصغير يستحق الإتقان أيضاً. حين تكون الأرقام راتب شخص ما، فضياع إدخال واحد غير مقبول.
- Launch is the start, not the end. The most useful features came from watching the app being used.La mise en ligne est un début, pas une fin. Les fonctionnalités les plus utiles sont venues en voyant l'appli utilisée.النشر بداية لا نهاية. أنفع الميزات جاءت من مشاهدة التطبيق وهو يُستعمل.
More screens and details in the case study, and the code on GitHub. Plus d'écrans et de détails dans l'étude de cas, et le code sur GitHub.مزيد من الشاشات والتفاصيل في دراسة الحالة، والشيفرة على GitHub.