Comprehensive Code Review

SIMKESGI - Clinical Dental Management System

6.5
Overall Score
2
Critical Issues
5
Severe Issues
8
Good Practices
πŸ“… Review Details

Prepared by: Senior Laravel Developer Perspective
Review Date: February 2026
Codebase: SIMKESGI (Clinical Dental Management System)

1 / 18

Executive Summary Overview

6.5/10
Functional but with architectural concerns
βœ… Good News

Application works, solves real problems, demonstrates clinical domain knowledge

❌ Bad News

Several patterns will make you look amateur in senior/architect interviews

πŸ”΄ CRITICAL

2 fatal security/data integrity issues that MUST be fixed immediately

Assessment Breakdown

πŸ”΄
CRITICAL
2
Fatal Issues
🟠
SEVERE
5
Major Issues
🟑
MODERATE
7
Minor Issues
🟒
GOOD
8
Best Practices
2 / 18

πŸ”΄ Critical Issue #1 Fatal

No Database Foreign Key Constraints

⚠️ Severity: 10/10 - CRITICAL

Data orphans WILL happen when concurrent deletes occur. No database-level protection against referential integrity violations.

What You Did

// Migration
$table->foreignId('kartupasien_id'); // Just creates integer column
// NO ->constrained() or ->references()

Why This Is FATAL

  • Data orphans WILL happen when concurrent deletes occur
  • No database-level protection against referential integrity violations
  • Race conditions: User A deletes kartupasien while User B adds diagnosis
  • Database cannot enforce relationships - relies 100% on application code
  • Backup/restore nightmares - orphaned records in production dumps

Real-World Scenario

Race Condition Example
// Time 10:00:01 AM
Student A: DELETE FROM odontograms WHERE id = 5

// Time 10:00:02 AM
Student B: INSERT INTO vitalitas (odontogram_id) VALUES (5)
β†’ Success! (No FK constraint)

// Result
βœ— Orphaned Vitalitas record with invalid odontogram_id
βœ— Application crashes when trying to display it

What Interviewers Will Think

πŸ’¬ Interviewer's Perspective

"Candidate doesn't understand database fundamentals. Would create data integrity disasters in production."

How to Fix

// Migration - ADD THIS
Schema::create('vitalitas', function (Blueprint $table) {
    $table->id();
    $table->foreignId('odontogram_id')
          ->constrained('odontograms')
          ->onDelete('cascade'); // Or 'restrict' for safety
    // ... rest
});
3 / 18

πŸ”΄ Critical Issue #2 Security

No CSRF Protection on AJAX Requests

⚠️ Severity: 9/10 - CRITICAL (if routes excluded from CSRF)

Potential Cross-Site Request Forgery vulnerability if routes are excluded from VerifyCsrfToken middleware.

What You Did

// View code - Missing CSRF token in AJAX
$.ajax({
    url: '/getPenyebabs',
    type: 'POST',
    data: { askepgilut: askepgilut }, // NO _token!
    cache: false,
    success: function(msg) { ... }
});

But I Saw This

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

The Problem

  • This $.ajaxSetup() is per-page setup
  • If forgotten on ONE page β†’ security hole
  • Laravel will reject without token (unless you excluded routes in VerifyCsrfToken.php)
  • Did you exclude these routes? If yes β†’ HUGE security vulnerability

What Interviewers Will Ask

πŸ’¬ Interview Question

"Show me your VerifyCsrfToken middleware exceptions list."

⚠️ If You Excluded Routes

If you excluded /getPenyebabs, /getGejalas from CSRF protection β†’ Cross-Site Request Forgery attacks are possible!

How to Fix (Proper Way)

// ALWAYS include token per request (safer than global setup)
$.ajax({
    url: '/getPenyebabs',
    type: 'POST',
    data: {
        _token: $('meta[name="csrf-token"]').attr('content'),
        askepgilut: askepgilut
    },
    success: function(msg) { ... }
});
βœ… Best Practice

Include _token explicitly in each AJAX request rather than relying on global $.ajaxSetup(). This ensures CSRF protection is never accidentally omitted.

4 / 18

🟠 Severe Issue #1 Architecture

String-Based Foreign Keys (pembimbing)

⚠️ Severity: 8/10 - SEVERE

Using string NIM instead of proper foreign key relationship

What You Did

// Migration
$table->string('pembimbing'); // Stores NIM as string!!!

Why This Is BAD

  • Not a real FK - can't join efficiently
  • No referential integrity - pembimbing can be deleted, string remains
  • String comparison slower than integer joins
  • Violates normalization - user data duplicated

Example of Problem

-- This query is SLOW (string comparison)
SELECT * FROM diagnosas 
WHERE pembimbing = '202301234';
-- vs proper FK (integer comparison + indexed)
SELECT * FROM diagnosas 
WHERE pembimbing_id = 15;

Proper Design

// Migration
$table->foreignId('pembimbing_id')
      ->constrained('users')
      ->onDelete('restrict'); // Prevent deleting active supervisor

// Model
public function pembimbing() {
    return $this->belongsTo(User::class, 'pembimbing_id');
}
πŸ’‘ Performance Impact

String comparisons are significantly slower than integer comparisons, especially with large datasets. Proper foreign keys also enable database-level indexing and query optimization.

5 / 18

🟠 Severe Issue #2 Database Design

Delimited String Storage (Violates 1NF)

⚠️ Severity: 8/10 - SEVERE

Violates First Normal Form - atomic values rule

What You Did

// diagnosas table
'askepgilut' => '1|2|3',
'penyebab'   => '1,2|3,4|5,6',
'gejala'     => '7,8|9,10|11,12'

Why This Is BAD

  • Violates First Normal Form - atomic values rule
  • Cannot query efficiently - must use LIKE '%1%' (no indexes)
  • Cannot enforce FK constraints - DB doesn't know '1' is askepgilut.id
  • Error-prone parsing - explode('|') logic fragile
  • Cannot aggregate - "How many diagnoses use askepgilut #3?" β†’ complex

Interviewer Will Ask

πŸ’¬ Interview Question

"How do you query: Show all diagnoses using penyebab_id = 5?"

Your Answer Would Be

// Horrible - full table scan
$diagnosas = Diagnosa::where('penyebab', 'LIKE', '%5%')->get();
// Also matches '15', '25', '51' β†’ WRONG RESULTS!

Proper Design

// Create pivot tables
Schema::create('diagnosa_askepgilut', function($table) {
    $table->foreignId('diagnosa_id')->constrained()->onDelete('cascade');
    $table->foreignId('askepgilut_id')->constrained()->onDelete('cascade');
    $table->primary(['diagnosa_id', 'askepgilut_id']);
});

// Model
class Diagnosa extends Model {
    public function askepgiluts() {
        return $this->belongsToMany(Askepgilut::class);
    }
}

// Clean queries
$diagnosa->askepgiluts; // Eloquent handles it
Askepgilut::find(5)->diagnosas; // Reverse query works
βœ… Acknowledged in Presentation

Good that you recognized this as technical debt in your presentation!

6 / 18

🟠 Severe Issue #3 Code Quality

Helper Functions in Global Namespace

⚠️ Severity: 7/10 - SEVERE

Pollutes global namespace and mixes concerns

What You Did

// helper.php
if (!function_exists('getPatients')) {
    function getPatients($user_id) {
        // Returns HTML string
        return '<option>...</option>';
    }
}

Why This Is BAD

  • Pollutes global namespace - name collision risk
  • Not testable - how do you unit test these?
  • Mixes concerns - business logic (query) + presentation (HTML)
  • Not reusable - tightly coupled to specific HTML structure

What Senior Devs Expect

// app/Services/PatientService.php
namespace App\Services;

class PatientService {
    public function getPatientsByUser(int $userId): Collection {
        return Kartupasien::where('user_id', $userId)->get();
    }
}

// app/View/Components/PatientSelect.php
class PatientSelect extends Component {
    public function __construct(public Collection $patients) {}
    
    public function render() {
        return view('components.patient-select');
    }
}

// Usage in Blade
<x-patient-select :patients="$patientService->getPatientsByUser($userId)" />

Benefits of Proper Approach

βœ…
Testable
βœ…
Separation of Concerns
βœ…
Reusable
βœ…
Type-Safe
7 / 18

🟠 Severe Issue #4 Validation

No Validation Classes (FormRequest)

⚠️ Severity: 7/10 - SEVERE

Validation rules scattered in controllers

What You Did

// Controller
public function store(Request $request) {
    // Inline validation or none at all
    $diagnosa = new Diagnosa();
    $diagnosa->askepgilut = $request->input('askepgilut');
    $diagnosa->save();
}

Why This Is BAD

  • No centralized validation - rules scattered in controllers
  • No reusability - can't reuse validation between store/update
  • Controller bloat - validation logic mixed with business logic
  • No custom messages - generic Laravel errors

Proper Pattern

// app/Http/Requests/StoreDiagnosaRequest.php
class StoreDiagnosaRequest extends FormRequest {
    public function authorize() {
        return Gate::allows('mahasiswa');
    }
    
    public function rules() {
        return [
            'diagnosa.*.askepgilut' => 'required|array|min:1',
            'diagnosa.*.askepgilut.*' => 'exists:askepgiluts,id',
            'diagnosa.*.penyebab' => 'required|array|min:1',
            'diagnosa.*.penyebab.*' => 'exists:penyebabs,id',
            'kartupasien_id' => 'required|exists:kartupasiens,id',
        ];
    }
    
    public function messages() {
        return [
            'diagnosa.*.askepgilut.required' => 'Pilih minimal 1 diagnosis',
        ];
    }
}

// Controller - CLEAN
public function store(StoreDiagnosaRequest $request) {
    // Request already validated, authorized
    $validated = $request->validated();
    // Business logic only
}
πŸ’‘ Best Practice

FormRequest classes keep controllers thin, centralize validation logic, and make code more maintainable and testable.

8 / 18

🟠 Severe Issue #5 Concurrency

Manual Cascade Deletes (Race Condition Risk)

⚠️ Severity: 7/10 - SEVERE

Manual cascade operations are not atomic

What You Did

public function destroy(Eksplakkal $eksplakkal) {
    Eksplakkal::destroy($eksplakkal->id);
    Periodontal::where('eksplakkal_id', $eksplakkal->id)->delete();
}

The Problem - Race Condition

Time User A User B
10:00 DELETE eksplakkal #5
10:01 (controller starts cascade) INSERT periodontal (eksplakkal_id=5)
10:02 DELETE periodontals WHERE... (Success - no FK constraint!)
10:03 (cascade complete)
10:04 (periodontal saved with dead eksplakkal)

Why This Happens

  • No database-level FK = no lock on parent delete
  • Two operations (delete parent, delete children) not atomic
  • Window of vulnerability between operations

Proper Solution

// Migration - Let DB handle it
$table->foreignId('eksplakkal_id')
      ->constrained()
      ->onDelete('cascade'); // Database handles cascade atomically

// Controller - Simple & Safe
public function destroy(Eksplakkal $eksplakkal) {
    $eksplakkal->delete(); // DB cascades automatically
}

Alternative: Use Transactions

public function destroy(Eksplakkal $eksplakkal) {
    DB::transaction(function() use ($eksplakkal) {
        Periodontal::where('eksplakkal_id', $eksplakkal->id)->delete();
        $eksplakkal->delete();
    });
}
9 / 18

🟑 Moderate Issues 7 Items

Summary of Moderate Priority Issues

1. No Service Layer
Fat controllers, business logic not separated
2. Inconsistent Naming Conventions
Mixed camelCase/snake_case, Indonesian/English
3. No Repository Pattern
Query logic mixed in controllers
4. Business Logic in Controller
ACC calculation should be in service/model
5. No Database Transactions
Multi-step operations not wrapped in transactions
6. Potential N+1 Queries
Missing eager loading in relationships
7. Magic Numbers in Code
Role values (1, 2, 3) not using enums/constants
πŸ“ Note

These issues won't break your application but will impact code maintainability, scalability, and team collaboration. They're important to address for professional-grade applications.

10 / 18

🟑 Moderate Issues Details Part 1

1. No Service Layer (Severity: 6/10)

❌ Current (Fat Controller)
public function store(Request $req) {
    $data = Kartupasien::where('user_id', auth()->id())
        ->with(['diagnosas', 'odontogram'])
        ->first();
    
    // Complex business logic here
    $dmft = $this->calculateDMFT($data);
    $ohis = $this->calculateOHIS($data);
    
    return view('result', compact('data', 'dmft'));
}
βœ… Better (Service Layer)
// Controller
public function store(
    Request $req, 
    PatientService $service
) {
    $patient = $service->getPatientData(auth()->id());
    $assessment = $service->calculateAssessment($patient);
    
    return view('result', compact('patient', 'assessment'));
}

// Service
class PatientService {
    public function calculateAssessment($patient) {
        return [
            'dmft' => $this->calculateDMFT($patient),
            'ohis' => $this->calculateOHIS($patient),
        ];
    }
}

2. Inconsistent Naming (Severity: 5/10)

// Mixed conventions
$table->string('nama_pasien');        // snake_case
public function getPasiens() { }      // camelCase + Indonesian
class kartupasien extends Model { }   // lowercase + Indonesian

// Should be
$table->string('patient_name');       // snake_case English
public function getPatients() { }     // camelCase English
class PatientCard extends Model { }   // PascalCase English

3. No Repository Pattern (Severity: 5/10)

// Repository Pattern
interface PatientRepositoryInterface {
    public function findByUser(int $userId): ?Kartupasien;
    public function getAllWithDiagnosis(): Collection;
}

class EloquentPatientRepository implements PatientRepositoryInterface {
    public function findByUser(int $userId): ?Kartupasien {
        return Kartupasien::where('user_id', $userId)
            ->with(['diagnosas', 'odontogram'])
            ->first();
    }
}
11 / 18

🟑 Moderate Issues Details Part 2

4. Business Logic in Controller (Severity: 6/10)

// ❌ Controller doing calculations
public function show($id) {
    $data = Periodontal::find($id);
    
    // Complex ACC calculation in controller
    $acc = 0;
    if ($data->pl_kuadran_1 + $data->di_kuadran_1 > 0) {
        $acc += 1;
    }
    // ... more calculation logic
    
    return view('show', compact('data', 'acc'));
}

// βœ… Move to Model or Service
class Periodontal extends Model {
    public function calculateACC(): int {
        $acc = 0;
        if ($this->pl_kuadran_1 + $this->di_kuadran_1 > 0) {
            $acc += 1;
        }
        // ... calculation logic
        return $acc;
    }
}

// Controller becomes clean
public function show($id) {
    $data = Periodontal::find($id);
    return view('show', [
        'data' => $data,
        'acc' => $data->calculateACC()
    ]);
}

5. No Database Transactions (Severity: 6/10)

// ❌ Without transaction
public function store(Request $request) {
    $kartupasien = Kartupasien::create($data);
    Odontogram::create(['kartupasien_id' => $kartupasien->id]);
    Periodontal::create(['kartupasien_id' => $kartupasien->id]);
    // If third create fails, you have orphaned records!
}

// βœ… With transaction
public function store(Request $request) {
    DB::transaction(function() use ($request) {
        $kartupasien = Kartupasien::create($data);
        Odontogram::create(['kartupasien_id' => $kartupasien->id]);
        Periodontal::create(['kartupasien_id' => $kartupasien->id]);
        // All or nothing - data integrity preserved
    });
}
12 / 18

🟑 Moderate Issues Details Part 3

6. Potential N+1 Queries (Severity: 5/10)

N+1 Query Problem
// Without eager loading
$ $kartupasiens = Kartupasien::all();
Query 1: SELECT * FROM kartupasiens

@foreach ($kartupasiens as $kp)
Query 2: SELECT * FROM users WHERE id = 1
Query 3: SELECT * FROM users WHERE id = 2
Query 4: SELECT * FROM users WHERE id = 3
// ... 100 more queries if you have 100 records!

// With eager loading
$ $kartupasiens = Kartupasien::with('user')->get();
Query 1: SELECT * FROM kartupasiens
Query 2: SELECT * FROM users WHERE id IN (1,2,3,...)
βœ“ Only 2 queries regardless of data size!

7. Magic Numbers (Severity: 4/10)

// ❌ Magic numbers
if (auth()->user()->role == 1) { }
Gate::define('adminpembimbing', fn($user) => in_array($user->role, [1,2]));

// βœ… Use Enums (PHP 8.1+)
enum UserRole: int {
    case ADMIN = 1;
    case PEMBIMBING = 2;
    case MAHASISWA = 3;
    case PATIENT = 4;
    case KARYAWAN = 5;
}

// Migration
$table->tinyInteger('role')->default(UserRole::MAHASISWA->value);

// Model
protected $casts = ['role' => UserRole::class];

// Usage
if (auth()->user()->role === UserRole::ADMIN) { ... }
13 / 18

🟒 Good Practices Keep These!

βœ… Things You Did RIGHT

8
Good Practices Found

1. Protected Routes with Middleware

Route::resource('/user', UserController::class)->middleware('admin');

2. Used Eloquent Relationships

public function kartupasien() {
    return $this->belongsTo(Kartupasien::class);
}

3. Gates for Authorization

Gate::define('adminpembimbing', fn($user) => in_array($user->role, [1,2]));

4. Resource Controllers (RESTful)

Route::resource('/diagnosa', DiagnosaController::class);

5. Dependency Injection

public function store(Request $request) // Laravel injects Request

6. Client-Side Calculations

  • βœ… Instant feedback for users
  • βœ… Good UX with immediate validation
  • βœ… Server still validates

7. Select2 Integration

  • βœ… Searchable dropdowns
  • βœ… Multi-select support
  • βœ… Better UX for large datasets

8. Real-World Domain Knowledge

  • βœ… DMF-T, OHI-S algorithms correct
  • βœ… Clinical workflow understood
  • βœ… Shows domain expertise
πŸ’ͺ Great Foundation!

These practices show you understand Laravel fundamentals and modern web development. Build on this foundation by addressing the critical and severe issues.

14 / 18

πŸ“Š Severity Breakdown Analysis

Severity Count Issues
πŸ”΄ CRITICAL 2 No FK constraints, CSRF potential
🟠 SEVERE 5 String FKs, delimited storage, helpers, no FormRequest, race conditions
🟑 MODERATE 7 No service layer, naming, no repos, ACC logic, transactions, N+1, magic numbers
🟒 GOOD 8 Middleware, relationships, gates, REST, DI, client calc, Select2, domain knowledge

Impact on Code Quality Score

Current: 6.5/10 β†’ After fixes: 8.5/10

Priority Matrix

πŸ”΄ Fix This Weekend (Critical)

1. Add Foreign Key Constraints (2 hours)
2. Fix CSRF on AJAX (30 mins)
3. Wrap Deletes in Transactions (1 hour)

🟠 Fix This Month (Severe)

1. Convert pembimbing to FK (4 hours)
2. Create FormRequest Classes (3 hours)
3. Fix Naming Conventions (2 hours)

15 / 18

🎯 Immediate Action Plan Roadmap

This Weekend (Critical Fixes)

1. Add Foreign Key Constraints
⏱️ 2 hours
php artisan make:migration add_foreign_keys_to_all_tables
2. Fix CSRF on AJAX
⏱️ 30 minutes
  • Verify no routes in VerifyCsrfToken exceptions
  • Add _token to all AJAX calls
3. Wrap Deletes in Transactions
⏱️ 1 hour
DB::transaction(function() { ... });

This Month (Severe Fixes)

1. Convert pembimbing to FK
⏱️ 4 hours
  • Create migration
  • Update queries
  • Test thoroughly
2. Create FormRequest Classes
⏱️ 3 hours
  • One per controller
  • Move validation rules
3. Fix Naming Conventions
⏱️ 2 hours
  • Rename models to PascalCase
  • English method names

Next Month (Architecture Improvements)

1. Refactor Diagnosa to Pivot Tables
⏱️ 8 hours
2. Extract Service Layer
⏱️ 6 hours
3. Add Repository Pattern
⏱️ 4 hours
16 / 18

πŸ—£οΈ How to Discuss in Interviews Strategy

When Asked About Weaknesses

❌ Don't Say

"Yeah, I know I should've used foreign keys but I forgot."

βœ… Do Say

"In this project, I initially chose application-level referential integrity for development flexibility. In retrospect, this was a mistakeβ€”database constraints would provide better data safety and are a lesson learned for production systems. In my next project, I implemented proper FK constraints from day one."

When Asked About String-Delimited Storage

βœ… Good Answer

"This was a conscious trade-off. The diagnosis data structure (multiple askepgiluts β†’ penyebabs β†’ gejala) required flexibility during early development when requirements changed frequently. I acknowledge this violates 1NF and isn't queryable efficiently. If I were to refactor, I'd implement proper pivot tablesβ€”I've documented the migration path in my technical debt backlog."

When Asked About Helpers

βœ… Good Answer

"These helper functions were a rapid prototyping decision. They work but aren't testable or reusable. I've since learned about Laravel's View Components and Service classesβ€”my more recent code follows those patterns."

πŸ’Ό Interviewer Red Flags

Things That Will Get You Rejected
  • ❌ Defending no FK constraints: "It's fine, my controller handles it" β†’ Shows lack of database fundamentals
  • ❌ Not knowing what 1NF is: If asked about delimited strings β†’ Shows lack of theoretical knowledge
  • ❌ Saying "it works fine in production": Without metrics β†’ Shows lack of rigor
  • ❌ Not having a refactoring plan: "I'd do it differently but haven't thought about how" β†’ Shows no growth mindset

✨ Things That Will Impress

Demonstrate Growth & Learning
  • βœ… Acknowledging mistakes proactively: "Here are 3 things I'd change..."
  • βœ… Showing evolution: "I learned X from this project, applied it to next one"
  • βœ… Having metrics: "This caused 3 orphaned records in 6 monthsβ€”acceptable for this use case"
  • βœ… Demonstrating business context: "Given limited time and changing requirements, this trade-off made sense"
17 / 18

πŸ“ˆ Final Verdict Summary

Your Code Level

Level Status
Junior Developer βœ… Definitely above this
Mid-Level Developer ⚠️ You're here, with rough edges
Senior Developer ❌ Not yetβ€”need architecture skills

What This Code Shows

βœ… Strengths
  • Can build working applications
  • Understands Laravel basics
  • Good domain knowledge
  • Uses modern tools (Select2, AJAX)
❌ Weaknesses
  • Database design fundamentals lacking
  • No architecture patterns (service/repo)
  • Security concerns (CSRF)
  • Scalability issues (N+1, no indexes)

Job Market Assessment

βœ… Can Get Hired As:
  • Junior Laravel Developer (1-2 years exp)
  • Web Developer (agency work)
  • Intern/Fresh Graduate (above average)
⚠️ Will Struggle For:
  • Senior Laravel Developer
  • Software Architect
  • Tech Lead positions
🎯 After Fixing Critical + Severe Issues:
  • Mid-Level Developer (2-4 years exp)
  • Backend Developer (product companies)

πŸŽ“ Learning Path (3 Months to Mid-Level)

Month 1: Database Mastery
Read "Database Design for Mere Mortals", Practice normalization, Fix all FK issues
Month 2: Laravel Architecture
Read "Domain-Driven Design in PHP", Watch Laracasts "SOLID Principles", Refactor services & repos
Month 3: Advanced Patterns
Read "Refactoring" by Martin Fowler, Build new feature with TDD, Learn Event Sourcing, CQRS

πŸ“ Conclusion

πŸ’ͺ You Have Potential!

Your code shows potential but has critical flaws that will hurt you in interviews. Good News: All fixable within 3 months of focused work.

6.5β†’8.5
Code Quality After Fixes
3 Mo
Time to Mid-Level
πŸš€ Remember

Every senior dev has written code like this. The difference is they learned from it. Show that growth in interviews and you'll do great!

Practice Questions for Interviews:

  • "Why didn't you use foreign key constraints?"
  • "How would you prevent N+1 queries here?"
  • "Walk me through how you'd refactor this diagnosis storage."
  • "What happens if two users delete the same record simultaneously?"

Have answers ready. Not excusesβ€”learning stories. πŸš€

18 / 18