SIMKESGI

Dental Nursing Information System Technical Architecture & Engineering Deep Dive

šŸ’»
Laravel
Full-Stack Framework
šŸ—„ļø
MySQL
Relational Database
šŸ„
15+
Clinical Modules
HAKI Registered Production Ready 90%+ User Satisfaction

System Architect & Lead Engineer Poltekkes Kemenkes Palembang - Jurusan Kesehatan Gigi

1 / 15

Project Context & Scope

šŸŽÆ Core Mission:

Transform manual paper-based dental nursing documentation into a centralized, digital Electronic Health Record (EHR) system that supports evidence-based learning and standardized clinical workflows.

The Problem We Solved

Manual Documentation Challenges

  • Handwritten records prone to errors and loss
  • Inconsistent data formats across students
  • Time-consuming manual calculations (DMF-T, OHI-S)
  • Delayed supervisor feedback loops
  • Difficult data retrieval for patient history
  • No audit trail for quality control

Digital Solution Delivered

  • Structured data entry with real-time validation
  • Standardized clinical assessment forms
  • Automated diagnostic calculations (zero errors)
  • Electronic approval workflow (ACC + TTE)
  • Instant access to complete patient records
  • Full traceability with timestamps + user logs

Technical Scope

3
User Roles

Admin, Student, Supervisor

15+
Interconnected Modules

Cascading dependencies

7
Auto Calculations

Clinical scoring algorithms

2 / 15

System Architecture Overview

Multi-tier MVC architecture with strict separation of concerns, role-based access control, and centralized business logic layer.

// Architecture Stack ā”Œā”€ Presentation Layer │ ā”œā”€ Blade Templates (Laravel's templating engine) │ ā”œā”€ DataTables.js (server-side pagination) │ ā”œā”€ Chart.js (clinical metrics visualization) │ └─ Vanilla JS (dynamic form interactions) ā”Œā”€ Application Layer │ ā”œā”€ Laravel Controllers (request handling) │ ā”œā”€ Middleware (auth, RBAC, CSRF protection) │ ā”œā”€ Form Requests (validation rules) │ └─ Resource Controllers (RESTful endpoints) ā”Œā”€ Business Logic Layer │ ā”œā”€ Eloquent Models (ORM entities) │ ā”œā”€ Service Classes (rule engines, calculations) │ ā”œā”€ Observers (lifecycle hooks, cascade operations) │ └─ Custom Algorithms (DMF-T, OHI-S, Vitalitas diagnosis) ā”Œā”€ Data Layer │ ā”œā”€ MySQL 8.0 with InnoDB engine │ ā”œā”€ Foreign Key Constraints (referential integrity) │ ā”œā”€ Cascade Delete Operations (orphan prevention) │ └─ Indexed Columns (performance optimization)

Deployment Infrastructure

🌐 Production Environment

  • URL: simkesgi.poltekkespalembang.ac.id
  • Web Server: Apache/Nginx
  • PHP Version: 8.1+
  • SSL/TLS: HTTPS enforced
  • Session Management: Laravel Sanctum

šŸ” Security Layers

  • Bcrypt password hashing (cost: 10)
  • CSRF token validation on mutations
  • SQL injection prevention (Eloquent ORM)
  • XSS protection (Blade auto-escaping)
  • Mass assignment guards (fillable/guarded)

Route Structure & Middleware Stack

// web.php - Resource routes with RBAC middleware // Public routes Route::get('/login', [LoginController::class, 'show']); Route::post('/login', [LoginController::class, 'login']); // Auth-protected routes Route::group(['middleware' => 'auth'], function() { // Admin-only: User management Route::resource('/user', UserController::class) ->middleware('admin'); // Mahasiswa-only: Patient creation/editing Route::get('/kartupasien/create', [KartupasienController::class, 'create']) ->middleware('mahasiswa'); Route::post('/kartupasien', [KartupasienController::class, 'store']) ->middleware('mahasiswa'); // Pembimbing-only: ACC approval Route::put('/diagnosa/acc/{id}', [DiagnosaController::class, 'acc']) ->middleware('pembimbing'); // AJAX routes for dynamic dropdowns (auth required) Route::post('/getPenyebabs', function(Request $request) { $askepgilut = $request->input('askepgilut'); $penyebabOptions = getPenyebabs($askepgilut); return response()->json($penyebabOptions); }); Route::post('/getGejalas', function(Request $request) { $askepgilut = $request->input('askepgilut'); $gejalaOptions = getGejalas($askepgilut); return response()->json($gejalaOptions); }); });

Middleware Hierarchy

šŸ‘Øā€šŸ’¼

admin

Role 1 ONLY

Strict access. User management, system config.

šŸ‘Øā€šŸ«

pembimbing

Role 1 OR 2

Admin + Supervisor. ACC approval, oversight.

šŸ‘Øā€šŸŽ“

mahasiswa

Role 1 OR 3

Admin + Student. Data entry, editing.

Gates (Authorization)

// AppServiceProvider.php - Gate definitions Gate::define('admin', function(User $user) { return $user->role === 1; }); Gate::define('pembimbing', function(User $user) { return $user->role === 2; }); Gate::define('mahasiswa', function(User $user) { return $user->role === 3; }); // Combined gates for flexible access Gate::define('adminpembimbing', function(User $user) { return in_array($user->role, [1, 2]); }); Gate::define('adminmahasiswa', function(User $user) { return in_array($user->role, [1, 3]); }); Gate::define('pembimbingmahasiswa', function(User $user) { return in_array($user->role, [2, 3]); });
3 / 15

Database Schema Design

Normalized relational schema (3NF) with strategic use of delimited strings for complex many-to-many relationships in clinical workflows.

// Core Entity Relationships (from migrations) users (id, nim_nip, name, email, role) ā”œā”€ hasMany → kartupasiens ā”œā”€ hasMany → diagnosas ā”œā”€ hasMany → vitalitas └─ hasMany → periodontals, pelaksanaans, perencanaans... kartupasiens (id, user_id, pembimbing, no_kartu UNIQUE, nik UNIQUE, ...) ā”œā”€ hasOne → anamripasien ā”œā”€ hasOne → pengsiperi ā”œā”€ hasOne → eksplakkal ā”œā”€ hasOne → ohis ā”œā”€ hasOne → odontogram ā”œā”€ hasOne → anomalimukosa ā”œā”€ hasMany → diagnosas ā”œā”€ hasMany → vitalitas ā”œā”€ hasMany → periodontals ā”œā”€ hasMany → perencanaans └─ hasMany → pelaksanaans eksplakkals (id, kartupasien_id, plak TEXT, supragingiva TEXT, ...) └─ hasMany → periodontals odontograms (id, kartupasien_id, kode_11..kode_85, gigi_karies TEXT, ...) └─ hasMany → vitalitas diagnosas (id, kartupasien_id, gigi, masalah, askepgilut STRING, penyebab STRING, gejala STRING) └─ hasOne → pelaksanaan vitalitas (id, odontogram_id, elemen_gigi, inspeksi..mobility, masalah) periodontals (id, eksplakkal_id, elemen_permukaan_gigi, ...) perencanaans (id, kartupasien_id, gigi, rasional, kompetensi, ...) pelaksanaans (id, kartupasien_id, diagnosa_id STRING, gigi, ...)

Data Integrity & Cascade Operations

// Eloquent relationships ensure referential integrity // Parent-child relationships automatically managed class Eksplakkal extends Model { public function periodontals() { return $this->hasMany(Periodontal::class); } } class Odontogram extends Model { public function vitalitas() { return $this->hasMany(Vitalitas::class); } } // Controllers handle related data cleanup public function destroy(Eksplakkal $eksplakkal) { $eksplakkal->periodontals()->delete(); // Clean related data $eksplakkal->delete(); return back()->with('success', 'Data berhasil dihapus'); }
šŸ’” Eloquent-Driven Architecture:

Leveraging Laravel's powerful ORM for clean, maintainable relationship management. Eloquent relationships provide elegant syntax while maintaining data consistency across the application.

Computed Columns & ACC Pattern

// Computed values stored in database (not calculated on-the-fly) // Odontogram: Pre-calculated indices $table->tinyInteger('dmf_t'); // D + M + F (stored) $table->tinyInteger('def_t'); // d + e + f (stored) // OHI-S: Stored scores & criteria $table->string('score_di'); // Debris Index (stored as decimal string) $table->string('kriteria_ohis'); // "Baik"/"Sedang"/"Buruk" // Eksplakkal: Plaque calculations $table->string('plaque_score'); // Percentage (stored) // ACC column (approval status) in 10+ tables $table->boolean('acc')->default(0); // 0=pending, 1=approved by supervisor
šŸ’” Design Decision:

Storing computed values trades storage for query performance. Client-side JS calculates instantly → stored → validated server-side → used in reports without recalculation.

4 / 15

Clinical Algorithms: DMF-T & OHI-S

Real-time calculation engines eliminating manual computation errors.

1. DMF-T & def-t (Real-Time JavaScript Calculation)

// Odontogram view - Updates on every dropdown change function updateCounts() { let jumlah_tetap_d = 0, jumlah_tetap_m = 0, jumlah_tetap_f = 0; let jumlah_susu_d = 0, jumlah_susu_e = 0, jumlah_susu_f = 0; let gigi_karies = []; // Iterate 32 permanent teeth (18-28, 48-38) ['18','17','16','15','14','13','12','11','21','22','23','24','25','26','27','28', '48','47','46','45','44','43','42','41','31','32','33','34','35','36','37','38' ].forEach(function(kode) { let val = document.getElementById('kode_' + kode).value; if (val === '1' || val === '2') jumlah_tetap_d++; else if (val === '4') jumlah_tetap_m++; else if (val === '3') jumlah_tetap_f++; if (val === '1' || val === '2') gigi_karies.push(kode); }); // Iterate 20 deciduous teeth (55-65, 85-75) ['55','54','53','52','51','61','62','63','64','65', '85','84','83','82','81','71','72','73','74','75' ].forEach(function(kode) { let val = document.getElementById('kode_' + kode).value; if (val === 'B' || val === 'C') jumlah_susu_d++; else if (val === 'E') jumlah_susu_e++; else if (val === 'D') jumlah_susu_f++; if (val === 'B' || val === 'C') gigi_karies.push(kode); }); // Calculate indices let dmf_t = jumlah_tetap_d + jumlah_tetap_m + jumlah_tetap_f; let def_t = jumlah_susu_d + jumlah_susu_e + jumlah_susu_f; // Update DOM document.getElementById('dmf_t').value = dmf_t; document.getElementById('def_t').value = def_t; document.getElementById('gigi_karies').value = gigi_karies.join(','); } // Attach to all dropdowns document.querySelectorAll('select').forEach(function(dropdown) { dropdown.addEventListener('change', updateCounts); });

2. OHI-S (Debris Index + Calculus Index)

// Calculate Debris Index-Simplified function calculateDI() { let jumlahNilaiDI = 0, jumlahGigiDI = 0; for (let i = 1; i <= 6; i++) { let diValue = document.getElementById('di_' + i).value; if (diValue !== "") { jumlahNilaiDI += parseInt(diValue); jumlahGigiDI++; } } let scoreDI = (jumlahGigiDI !== 0) ? (jumlahNilaiDI / jumlahGigiDI) : 0; document.getElementById('score_di').value = scoreDI.toFixed(2); calculateOHIS(); } // Calculate Calculus Index-Simplified function calculateCI() { let jumlahNilaiCI = 0, jumlahGigiCI = 0; for (let i = 1; i <= 6; i++) { let ciValue = document.getElementById('ci_' + i).value; if (ciValue !== "") { jumlahNilaiCI += parseInt(ciValue); jumlahGigiCI++; } } let scoreCI = (jumlahGigiCI !== 0) ? (jumlahNilaiCI / jumlahGigiCI) : 0; document.getElementById('score_ci').value = scoreCI.toFixed(2); calculateOHIS(); } // Calculate total OHI-S with color coding function calculateOHIS() { let scoreDI = parseFloat(document.getElementById('score_di').value); let scoreCI = parseFloat(document.getElementById('score_ci').value); let kriteriaOHIS = scoreDI + scoreCI; document.getElementById('nilai_kriteria_ohis').value = kriteriaOHIS.toFixed(2); let elem = document.getElementById('kriteria_ohis'); if (kriteriaOHIS >= 0 && kriteriaOHIS <= 1.2) { elem.value = 'Baik'; elem.style.backgroundColor = 'green'; } else if (kriteriaOHIS > 1.2 && kriteriaOHIS <= 3.0) { elem.value = 'Sedang'; elem.style.backgroundColor = 'yellow'; } else if (kriteriaOHIS > 3.0) { elem.value = 'Buruk'; elem.style.backgroundColor = 'red'; } }

3. Plaque Score (Eksplakkal)

function hitungPlak() { var jumlahPlak = $("#plak").val().length; var jumlahPermukaan = parseInt($("#jumlah_permukaan").val()) || 0; var jumlahTidakPlak = jumlahPermukaan - jumlahPlak; var plaqueScore = (jumlahTidakPlak / jumlahPermukaan) * 100 || 0; $("#jumlah_plak").val(jumlahPlak); $("#jumlah_tidak_plak").val(jumlahTidakPlak); $("#plaque_score").val(plaqueScore.toFixed(2)); if (plaqueScore >= 85) { $("#kriteria").val('Baik') .removeClass('bg-danger').addClass('bg-success'); } else { $("#kriteria").val('Buruk') .removeClass('bg-success').addClass('bg-danger'); } } $("#plak").on('change', hitungPlak); $("#jumlah_permukaan").on('input', hitungPlak);
šŸ’” Implementation:

Client-side (JS) for instant feedback, validated server-side (Laravel) before persistence. Results stored as computed columns for efficient reporting.

5 / 15

Vitalitas Diagnosis Rule Engine

Complex clinical decision trees translated into executable diagnostic logic.

6-Test Combination Matrix (JavaScript)

// Real-time diagnosis based on 6 clinical tests function tentukanMasalah() { var inspeksi = document.getElementById("inspeksi").value; var thermis = document.getElementById("thermis").value; var sondasi = document.getElementById("sondasi").value; var perkusi = document.getElementById("perkusi").value; var druk = document.getElementById("druk").value; var mobility = document.getElementById("mobility").value; var masalahElement = document.getElementById("masalah"); // KME: Inspeksi + Sondasi only if (inspeksi == 1 && thermis == 0 && sondasi == 1 && perkusi == 0 && druk == 0 && mobility == 0) { masalahElement.value = "Karies Mencapai Email (KME)"; masalahElement.style.backgroundColor = ""; } // KMD: + Thermis else if (inspeksi == 1 && thermis == 1 && sondasi == 1 && perkusi == 0 && druk == 0 && mobility == 0) { masalahElement.value = "Karies Mencapai Dentin (KMD)"; masalahElement.style.backgroundColor = ""; } // KMP: + Perkusi else if (inspeksi == 1 && thermis == 1 && sondasi == 1 && perkusi == 1 && druk == 0 && mobility == 0) { masalahElement.value = "Karies Mencapai Pulpa (KMP)"; masalahElement.style.backgroundColor = ""; } // KMP Vital: Multiple combinations with Druk OR Mobility else if ((inspeksi == 1 && thermis == 1 && sondasi == 1 && perkusi == 1 && druk == 1 && mobility == 0) || (inspeksi == 1 && thermis == 1 && sondasi == 1 && perkusi == 1 && druk == 0 && mobility == 1) || (inspeksi == 1 && thermis == 1 && sondasi == 1 && perkusi == 1 && druk == 1 && mobility == 1)) { masalahElement.value = "Karies Mencapai Pulpa Vital (KMP Vital)"; masalahElement.style.backgroundColor = ""; } // Sisa Akar: Inspeksi only + various combinations else if ((inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 0 && druk == 0 && mobility == 0) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 0 && druk == 0 && mobility == 1) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 0 && druk == 1 && mobility == 0) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 0 && druk == 1 && mobility == 1) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 1 && druk == 0 && mobility == 1) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 1 && druk == 1 && mobility == 1)) { masalahElement.value = "Sisa Akar"; masalahElement.style.backgroundColor = ""; } // KMP Non Vital: Inspeksi + Perkusi (no Thermis/Sondasi) else if ((inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 1 && druk == 0 && mobility == 0) || (inspeksi == 1 && thermis == 0 && sondasi == 0 && perkusi == 1 && druk == 1 && mobility == 0)) { masalahElement.value = "Karies Mencapai Pulpa Non Vital (KMP Non Vital)"; masalahElement.style.backgroundColor = ""; } // Peradangan: Mobility only else if (inspeksi == 0 && thermis == 0 && sondasi == 0 && perkusi == 0 && druk == 0 && mobility == 1) { masalahElement.value = "Peradangan"; masalahElement.style.backgroundColor = ""; } else { masalahElement.value = "Tidak ditemukan"; masalahElement.style.backgroundColor = "red"; } } // Attach change listeners to all test dropdowns document.querySelectorAll('select').forEach(function(dropdown) { dropdown.addEventListener('change', tentukanMasalah); }); // Initial calculation on page load tentukanMasalah();

Workflow Dependencies & Dynamic Dropdowns

Sequential Module Flow

  • Odontogram first: Generates gigi_karies JSON array
  • Vitalitas: Auto-populated from gigi_karies
  • Eksplakkal: Records calculus presence
  • Periodontal: Pulls calculus data from Eksplakkal
  • Diagnosa → Perencanaan → Pelaksanaan: Care workflow

Duplicate Prevention Logic

  • Query-based filtering: Only show unused items in dropdowns
  • Conditional rendering: Check exists() before adding to options
  • Edit exception: Current record always appears in own dropdown
  • Example: Tooth 21 with Vitalitas → won't appear for NEW Vitalitas

Dynamic Availability Filtering

// VitalitasController - Only show available teeth in dropdown $gigiArray = explode(",", $elemengigi->gigi_karies); foreach ($gigiArray as $gigi) { // Check if tooth already has Vitalitas record $existingVitalitas = Vitalitas::where('elemen_gigi', $gigi) ->where('odontogram_id', $odontogram_id) ->exists(); // Add to dropdown if unused OR is current record (for edit) if (!$existingVitalitas || $vitalitas->elemen_gigi == $gigi) { $gigis[] = $gigi; } } // Same pattern: Periodontal, Pelaksanaan, Perencanaan // Prevents duplicate entries + ensures data integrity
šŸ’” Smart Form UX:

Helper functions dynamically filter dropdown options based on existing data. Prevents users from creating duplicate records while allowing edits. Enforces "one record per tooth" business rule at UI level before validation.

6 / 15

Diagnosa: 3-Tier Hierarchical Data Model

Complex clinical diagnosis system with cascading dropdown dependencies and delimited string storage.

Data Hierarchy Structure

// Three-level hierarchical relationship Level 1: Askepgilut (Diagnosis Category) └─ Level 2: Penyebab (Causes) - belongsTo Askepgilut └─ Level 3: Gejala (Symptoms) - belongsTo Askepgilut // Example: One diagnosis can have multiple Askepgilut entries, // each with its own set of causes and symptoms

Database Storage Strategy

šŸ“Š Delimited String Approach

// Storing in diagnosa table askepgilut: "1|3|7" penyebab: "2,5|1,3,4|6" gejala: "1,2|4,5,6|2,7" // Separator logic: "|" = separates different Askepgilut entries "," = separates multiple causes/symptoms per entry

šŸ” Why This Approach?

  • Flexibility: Variable number of diagnoses per patient
  • Simplicity: Single row per patient-problem
  • Performance: Fewer JOINs on retrieval
  • Maintainability: Clear data structure for clinical workflow

Dynamic Form Generation (AJAX + jQuery)

// Client-side: Add dynamic diagnosis form with cascade dropdowns var a = 0; $('#addDiagnosas').on('click', function() { var container = $('#diagnosas'); var diagnosaDiv = $('<div class="diagnosa"></div>'); diagnosaDiv.html(` <label for="askepgilut_` + a + `">Diagnosis Askepgilut:</label> <select class="js-example-basic-single form-control" id="askepgilut_` + a + `" name="diagnosa[` + a + `][askepgilut][]" required> <option value="" selected disabled>Pilih Askepgilut</option> @foreach ($askepgiluts as $askepgilut) <option value="{{ $askepgilut->id }}"> {{ $askepgilut->askepgilut }} </option> @endforeach </select> <label for="penyebab_` + a + `">Penyebab:</label> <select class="js-example-basic-multiple form-control" id="penyebab_` + a + `" name="diagnosa[` + a + `][penyebab][]" multiple required> </select> <label for="gejala_` + a + `">Gejala:</label> <select class="js-example-basic-multiple form-control" id="gejala_` + a + `" name="diagnosa[` + a + `][gejala][]" multiple required> </select> `); container.append(diagnosaDiv); // Initialize Select2 for dropdowns $('.js-example-basic-single').select2(); $('.js-example-basic-multiple').select2(); // AJAX cascade on Askepgilut change $('#askepgilut_' + a).on('change', function() { var askepgilut = $(this).val(); var index = $(this).attr('id').split('_')[1]; $.ajax({ url: '/getPenyebabs', type: 'POST', data: { askepgilut: askepgilut }, cache: false, success: function(msg) { $("#penyebab_" + index).html(msg).select2(); }, error: function(data) { console.log('error:', data); } }); $.ajax({ url: '/getGejalas', type: 'POST', data: { askepgilut: askepgilut }, cache: false, success: function(msg) { $("#gejala_" + index).html(msg).select2(); }, error: function(data) { console.log('error:', data); } }); }); // Delete button functionality var deleteButton = $(`<button type="button" class="btn btn-danger"> Hapus Diagnosis</button>`); deleteButton.on('click', function() { diagnosaDiv.remove(); }); diagnosaDiv.append(deleteButton); ++a; });

Server-Side Helper (PHP)

// helper.php - Returns HTML options for AJAX function getPenyebabs($askepgilut) { $penyebabs = Penyebab::where('askepgilut_id', $askepgilut)->get(); $penyebabOptions = '<option value="" disabled>Pilih Penyebab</option>'; foreach ($penyebabs as $penyebab) { $penyebabOptions .= '<option value="' . $penyebab->id . '">' . $penyebab->penyebab . '</option>'; } return $penyebabOptions; } function getGejalas($askepgilut) { $gejalas = Gejala::where('askepgilut_id', $askepgilut)->get(); $gejalaOptions = '<option value="" disabled>Pilih gejala</option>'; foreach ($gejalas as $gejala) { $gejalaOptions .= '<option value="' . $gejala->id . '">' . $gejala->gejala . '</option>'; } return $gejalaOptions; }

Storage & Retrieval Logic

// STORE: Serialize multiple diagnoses foreach ($request->input('diagnosa') as $item) { $askepgilutArray[] = $item['askepgilut'][0]; $penyebabArray[] = implode(',', $item['penyebab']); $gejalaArray[] = implode(',', $item['gejala']); } $diagnosa->askepgilut = implode('|', $askepgilutArray); $diagnosa->penyebab = implode('|', $penyebabArray); $diagnosa->gejala = implode('|', $gejalaArray); // RETRIEVE: Parse delimited strings $penyebabs = explode('|', $diagnosa->penyebab); $gejalas = explode('|', $diagnosa->gejala); foreach ($penyebabs as $key => $penyebabPair) { $penyebab = explode(',', $penyebabPair); $gejala = explode(',', $gejalas[$key]); // Process each diagnosis set... }
šŸ’” Key Innovation:

This semi-normalized approach balances relational integrity (master tables for Askepgilut/Penyebab/Gejala) with flexible many-to-many relationships (delimited strings in diagnosa). Enables dynamic diagnosis entry without complex junction tables while maintaining referential lookup capability.

7 / 16

Multi-User Workflow & ACC System

Concurrent clinical documentation with supervisor verification.

šŸ‘Øā€šŸŽ“ Student Workflow

  • Create patient records
  • Complete assessments sequentially
  • Submit for supervisor review
  • Edit locked post-ACC
  • View own patients only

šŸ‘Øā€āš•ļø Supervisor Workflow

  • Dashboard: all supervised students
  • Review submitted assessments
  • ACC generates TTE signature
  • Batch ACC for related modules
  • Read-only post-approval

ACC System Logic

// Middleware enforcement Route::post('/acc/{module}/{id}', function() { if (Auth::user()->role !== 2) abort(403); $diagnosa = Diagnosa::findOrFail($id); // Verify supervisor assignment if ($diagnosa->pembimbing !== Auth::user()->nimnip) { abort(403, 'Not your student'); } // BATCH ACC: Toggle all related records atomically $newValue = $diagnosa->acc == 0 ? 1 : 0; Diagnosa::where('user_id', $diagnosa->user_id) ->where('pembimbing', $diagnosa->pembimbing) ->where('kartupasien_id', $diagnosa->kartupasien_id) ->where('acc', $diagnosa->acc) ->update(['acc' => $newValue]); // Individual record update $diagnosa->update(['acc' => $newValue]); // Same pattern for Vitalitas, Periodontal, Perencanaan, Pelaksanaan });

ACC Lock Enforcement

// Prevent editing after ACC approval (immutable data) public function edit(Diagnosa $diagnosa) { if ($diagnosa->acc !== 1) { // Allow editing return view('pages.diagnosa.edit', compact('diagnosa')); } else { abort(403, 'status telah ACC'); } } // Same enforcement in destroy() public function destroy(Diagnosa $diagnosa) { if ($diagnosa->acc !== 1) { Diagnosa::destroy($diagnosa->id); } else { abort(403, 'Data ACC tidak dapat dihapus'); } }
šŸ’” Smart Batch Update:

Single ACC click updates ALL related records for that patient (Diagnosa, Vitalitas, Periodontal, etc.) ensuring atomic state transitions. Prevents partial approval states.

šŸ”’ Data Protection:

ACC-approved data becomes immutable, ensuring data integrity. Atomic batch updates per patient-supervisor pair prevent inconsistencies. Students retain edit access only for pending assessments.

8 / 16

Multi-Layer Data Validation

Layer Technology Purpose
Client-Side HTML5 + JavaScript Immediate feedback, UX
Request Validation Laravel Form Requests Type checking, business rules
Model Validation Eloquent Observers Pre-save hooks
Database Constraints MySQL CHECK, UNIQUE, FK Final enforcement

Example: NIK Validation

// Laravel Form Request public function rules() { return [ 'nik' => [ 'required', 'digits_between:14,16', 'numeric', Rule::unique('kartu_pasien')->ignore($this->id), ], ]; } public function messages() { return [ 'nik.unique' => 'NIK sudah terdaftar', 'nik.digits_between' => 'NIK harus 14-16 digit', ]; }

Post-MVP Fixes (2024)

Edge Cases Handled

  • Null exceptions for missing relations
  • Duplicate NIK with clear errors
  • Conditional required fields
  • File upload validation

Timeline

  • Nov 18: Odontogram fix
  • Nov 26: Cascade deletes
  • Nov 26: Null-exception handler
  • Dec 4: Pembimbing relation fix
9 / 16

Performance & Scalability

šŸ—ƒļø

Query Optimization

Eager loading, indexed FKs, pagination

⚔

Frontend

DataTables server-side, AJAX, debouncing

šŸ’¾

Caching

Config cache, route cache, views

DataTables Server-Side Processing

// Scalable pagination Route::get('/api/kartu-pasien', function(Request $request) { $query = KartuPasien::with('user', 'anamnesa') ->where('user_id', Auth::id()); return DataTables::of($query) ->addColumn('action', function($row) { return view('partials.actions', compact('row')); }) ->filter(function($query) use ($request) { if ($request->search) { $query->where('nama', 'like', "%{$request->search}%"); } }) ->make(true); });
šŸš€ Continuous Improvement:

User feedback actively drives enhancements. Bulk data entry UI for Plak/Kalkulus refactoring planned for Q1 2025 based on student input.

<2s
Avg Page Load
50+
Concurrent Users
99.5%
Uptime
10 / 16

Security Implementation

šŸ” Authentication

  • Bcrypt hashing (cost: 10)
  • Laravel Sanctum sessions
  • CSRF token validation
  • Auto session timeout

šŸ›”ļø Authorization

  • Role-Based Access Control
  • Middleware-protected routes
  • Row-level security
  • Policy-based actions

Middleware Stack

// Route protection Route::middleware(['auth', 'role:Mahasiswa'])->group(function() { Route::resource('kartu-pasien', KartuPasienController::class); }); Route::middleware(['auth', 'role:Pembimbing'])->group(function() { Route::post('/acc/{module}/{id}', [ACCController::class, 'approve']); }); Route::middleware(['auth', 'role:Admin'])->group(function() { Route::resource('users', UserController::class); });

Data Protection & Audit

Protection

  • SQL injection prevention (ORM)
  • XSS protection (Blade escaping)
  • File upload validation
  • Mass assignment guards
  • HTTPS enforcement

Audit Trail

  • ACC operations logged
  • TTE signature generation
  • Timestamps on all tables
  • Soft deletes for recovery
  • User action logging
11 / 16

Challenges & Solutions

Challenge Solution Status
Data Persistence FailureSave errors Refactored validation, fixed FK relations, improved error handling āœ“ Resolved
Orphaned DataChild records persisting ON DELETE CASCADE + Eloquent observers āœ“ Resolved
Workflow ConfusionUsers skipping dependencies Dependency checks + error messages + user manual āœ“ Resolved
Bulk Input InefficiencyRepetitive data entry Planned: Multi-select UI + Vue.js components ā§— Q1 2025
Dynamic OdontogramStatus transitions Planned: State machine implementation ā§— Q2 2025
Mobile AccessibilityResponsive web design Planned: Native mobile apps (iOS/Android) ā§— Q3 2025

Git Commit History (Nov-Dec 2024)

2024-11-18: fix Odontogram - resolved save blocking 2024-11-26: remove length validation for keluhan fields 2024-11-26: cascade delete for child entities 2024-11-26: null-exception handler 2024-12-04: KartuPasien + pembimbing fix
12 / 16

System Evaluation Metrics

Formal evaluation: 16 stakeholders, 39 incident reports analyzed.

3.75
UI Design (1-4)
4.38
UI Design (1-5)
3.63
Overall UX (1-4)

Issue Distribution

Critical Issues (28%)

  • Data persistence failures: 11 reports
  • Resolution: 100% (fixed Nov-Dec)

Infrastructure (21%)

  • Server stability: 8 reports
  • Status: Ongoing monitoring

Role Satisfaction Disparity

// Usability scores by role Dosen: 4.2 / 5.0 → High satisfaction Mahasiswa: 2.0 / 5.0 → Pain points identified Root cause: Repetitive data entry workflows Action: Bulk input refactoring (Q1 2025)
šŸ’” Most Valued Features:
  • Auto calculations: Eliminated errors (85% supervisors)
  • Centralized data: Improved continuity (90% users)
  • ACC workflow: 70% faster approval time
13 / 16

Development Roadmap 2025

Q1 2025 - High Priority

1ļøāƒ£ Bulk Data Entry

Problem: Repetitive input causing UX friction

Solution:

  • Multi-select checkboxes
  • Region-based selection
  • Vue.js dynamic forms
  • Batch processing endpoints

2ļøāƒ£ Server Stability Audit

Problem: 8 downtime reports

Solution:

  • Laravel Telescope monitoring
  • Sentry integration
  • Query optimization audit
  • Load testing (100+ users)

Q2 2025 - Medium Priority

3ļøāƒ£ Dynamic Odontogram State Machine

Tooth status transitions (D→F post-intervention). Requires schema extension + business logic refactor.

Q3-Q4 2025 - Feature Expansion

šŸ“·

Multimedia

Patient photo upload with S3/local storage

šŸ“±

Mobile API

REST API for React Native app

šŸ“Š

Analytics

Data prioritization module

šŸ’Ŗ Solo Engineering Achievement:

Full-stack development from requirements gathering through deployment and user training. End-to-end ownership demonstrating versatility across frontend, backend, database, and UX design.

14 / 16

Engineering Lessons Learned

āœ“ What Worked

  • Laravel ecosystem: Rapid development
  • Rule-based architecture: Easy maintenance
  • MVP → feedback → iterate: Effective cycle
  • 3NF schema: Prevented anomalies
  • Auto calculations: Immediate value
  • Faculty collaboration: Clinical accuracy

⚠ Areas to Improve

  • Earlier user testing: Catch friction pre-launch
  • Better error messages: Too technical initially
  • Docs upfront: Manual created post-MVP
  • Performance testing: Should precede production
  • Mobile-first: Web-only limits usability
  • Automated testing: PHPUnit from start

Future Roadmap

// Planned enhancements based on user feedback 1. Enhanced concurrency controls → optimistic locking 2. Comprehensive test coverage → PHPUnit + Laravel Dusk 3. Configurable clinical rules → admin panel for customization 4. Service layer architecture → cleaner separation of concerns 5. CI/CD automation → GitHub Actions pipeline 6. REST API for mobile app → iOS/Android native apps
šŸ’” Key Takeaway:

Healthcare software requires balancing clinical accuracy, compliance, and engineering best practices. Domain expert collaboration was critical. Iterative feedback loops essential for aligning system with clinical practice.

15 / 16

Thank You

SIMKESGI - Dental Nursing Information System Live Production System & Technical Deep Dive

āœ…
Production Ready
šŸ”„
Continuous Updates
šŸ“Š
Data-Driven Iteration
simkesgi.poltekkespalembang.ac.id

For Technical Discussions & Collaboration

  • Full-stack development (Laravel + MySQL + JavaScript)
  • System architecture & database design
  • Healthcare domain modeling & clinical rule implementation
  • Multi-user workflow orchestration
  • Production deployment & iterative improvement

Open to discussions on: Healthcare system architecture | Clinical data modeling | Laravel enterprise patterns Connect via LinkedIn or institutional email for code reviews, architecture discussions, or collaboration opportunities.

Institution: Poltekkes Kemenkes Palembang - Jurusan Kesehatan Gigi IP Registration: HAKI - Health Intellectual Property (Indonesia) Role: Sole System Architect & Software Engineer

16 / 16