Skip to content

Coding Standards

This document outlines the coding standards and best practices for the Stratpoint Timesheet Application development team. These standards ensure code consistency, maintainability, and quality across the entire codebase.

General Principles

Code Quality Principles

  1. Readability: Code should be self-documenting and easy to understand
  2. Consistency: Follow established patterns and conventions
  3. Maintainability: Write code that is easy to modify and extend
  4. Performance: Consider performance implications of code decisions
  5. Security: Follow secure coding practices
  6. Testability: Write code that is easy to test

SOLID Principles

  • Single Responsibility: Each class should have one reason to change
  • Open/Closed: Open for extension, closed for modification
  • Liskov Substitution: Derived classes must be substitutable for base classes
  • Interface Segregation: Clients should not depend on interfaces they don't use
  • Dependency Inversion: Depend on abstractions, not concretions

PHP/Laravel Standards

PSR Standards Compliance

The codebase follows PSR (PHP Standards Recommendations):

  • PSR-1: Basic Coding Standard
  • PSR-2: Coding Style Guide (superseded by PSR-12)
  • PSR-4: Autoloader Standard
  • PSR-12: Extended Coding Style Guide

File Structure and Naming

<?php

namespace App\Services\Timesheet;

use App\Models\User;
use App\Models\Project;
use App\Exceptions\TimesheetException;
use Illuminate\Support\Collection;

/**
 * Timesheet calculation service
 * 
 * Handles all timesheet-related calculations including
 * utilization rates, overtime, and billing calculations.
 */
class TimesheetCalculationService
{
    private const DEFAULT_HOURS_PER_DAY = 8;
    private const OVERTIME_THRESHOLD = 40;

    private UserRepository $userRepository;
    private ProjectRepository $projectRepository;

    public function __construct(
        UserRepository $userRepository,
        ProjectRepository $projectRepository
    ) {
        $this->userRepository = $userRepository;
        $this->projectRepository = $projectRepository;
    }

    /**
     * Calculate utilization rate for a user in a given period
     *
     * @param User $user The user to calculate for
     * @param Carbon $startDate Start of the period
     * @param Carbon $endDate End of the period
     * @return float Utilization rate as percentage (0-100)
     * @throws TimesheetException When calculation fails
     */
    public function calculateUtilizationRate(
        User $user,
        Carbon $startDate,
        Carbon $endDate
    ): float {
        $workingDays = $this->getWorkingDays($startDate, $endDate);
        $expectedHours = $workingDays * self::DEFAULT_HOURS_PER_DAY;

        $actualHours = $this->getTotalHours($user, $startDate, $endDate);

        if ($expectedHours === 0) {
            throw new TimesheetException('No working days in the specified period');
        }

        return round(($actualHours / $expectedHours) * 100, 2);
    }

    /**
     * Get total hours worked by user in period
     */
    private function getTotalHours(User $user, Carbon $startDate, Carbon $endDate): float
    {
        return $user->timelogs()
            ->whereBetween('log_date', [$startDate, $endDate])
            ->sum('spent_hours');
    }

    /**
     * Calculate working days between two dates
     */
    private function getWorkingDays(Carbon $startDate, Carbon $endDate): int
    {
        $workingDays = 0;
        $current = $startDate->copy();

        while ($current->lte($endDate)) {
            if ($current->isWeekday() && !$this->isHoliday($current)) {
                $workingDays++;
            }
            $current->addDay();
        }

        return $workingDays;
    }

    /**
     * Check if date is a holiday
     */
    private function isHoliday(Carbon $date): bool
    {
        // Implementation for holiday checking
        return false;
    }
}

Laravel-Specific Standards

Controller Standards

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Timesheet\StoreTimesheetRequest;
use App\Http\Requests\Timesheet\UpdateTimesheetRequest;
use App\Http\Resources\TimesheetResource;
use App\Services\TimesheetService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class TimesheetController extends Controller
{
    private TimesheetService $timesheetService;

    public function __construct(TimesheetService $timesheetService)
    {
        $this->timesheetService = $timesheetService;
        $this->middleware('auth:api');
        $this->middleware('can:view,timesheet')->only(['show']);
        $this->middleware('can:update,timesheet')->only(['update']);
    }

    /**
     * Display a listing of timesheets
     */
    public function index(Request $request): JsonResponse
    {
        $timesheets = $this->timesheetService->getUserTimesheets(
            $request->user(),
            $request->get('start_date'),
            $request->get('end_date')
        );

        return response()->json([
            'data' => TimesheetResource::collection($timesheets),
            'meta' => [
                'total' => $timesheets->count(),
                'period' => [
                    'start' => $request->get('start_date'),
                    'end' => $request->get('end_date'),
                ],
            ],
        ]);
    }

    /**
     * Store a newly created timesheet entry
     */
    public function store(StoreTimesheetRequest $request): JsonResponse
    {
        $timesheet = $this->timesheetService->createTimesheet(
            $request->user(),
            $request->validated()
        );

        return response()->json([
            'data' => new TimesheetResource($timesheet),
            'message' => 'Timesheet entry created successfully',
        ], 201);
    }
}

Model Standards

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Carbon\Carbon;

/**
 * Timesheet model
 * 
 * @property int $id
 * @property int $user_id
 * @property int $project_id
 * @property Carbon $log_date
 * @property float $spent_hours
 * @property string $description
 * @property bool $is_billable
 * @property string $approval_status
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @property Carbon|null $deleted_at
 */
class Timesheet extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'timelogs';

    protected $fillable = [
        'user_id',
        'project_id',
        'log_date',
        'spent_hours',
        'description',
        'is_billable',
        'approval_status',
    ];

    protected $casts = [
        'log_date' => 'date',
        'spent_hours' => 'float',
        'is_billable' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    protected $attributes = [
        'approval_status' => 'pending',
        'is_billable' => true,
    ];

    // Constants
    public const STATUS_PENDING = 'pending';
    public const STATUS_APPROVED = 'approved';
    public const STATUS_REJECTED = 'rejected';

    public const STATUSES = [
        self::STATUS_PENDING,
        self::STATUS_APPROVED,
        self::STATUS_REJECTED,
    ];

    /**
     * Get the user that owns the timesheet
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get the project for this timesheet
     */
    public function project(): BelongsTo
    {
        return $this->belongsTo(Project::class);
    }

    /**
     * Scope for approved timesheets
     */
    public function scopeApproved($query)
    {
        return $query->where('approval_status', self::STATUS_APPROVED);
    }

    /**
     * Scope for billable timesheets
     */
    public function scopeBillable($query)
    {
        return $query->where('is_billable', true);
    }

    /**
     * Check if timesheet is approved
     */
    public function isApproved(): bool
    {
        return $this->approval_status === self::STATUS_APPROVED;
    }

    /**
     * Get formatted hours
     */
    public function getFormattedHoursAttribute(): string
    {
        return number_format($this->spent_hours, 2);
    }
}

Database Standards

Migration Standards

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('timelogs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade');
            $table->foreignId('project_id')
                  ->constrained()
                  ->onDelete('cascade');
            $table->date('log_date');
            $table->decimal('spent_hours', 5, 2);
            $table->text('description')->nullable();
            $table->boolean('is_billable')->default(true);
            $table->enum('approval_status', ['pending', 'approved', 'rejected'])
                  ->default('pending');
            $table->foreignId('approved_by')
                  ->nullable()
                  ->constrained('users')
                  ->onDelete('set null');
            $table->timestamp('approved_at')->nullable();
            $table->timestamps();
            $table->softDeletes();

            // Indexes
            $table->index(['user_id', 'log_date']);
            $table->index(['project_id', 'log_date']);
            $table->index(['approval_status', 'log_date']);
            $table->index('is_billable');

            // Unique constraint
            $table->unique(['user_id', 'project_id', 'log_date'], 'unique_user_project_date');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('timelogs');
    }
};

JavaScript/AngularJS Standards

File Structure and Naming

// app/modules/timesheet/controllers/TimesheetController.js

(function() {
    'use strict';

    angular
        .module('timesheet')
        .controller('TimesheetController', TimesheetController);

    TimesheetController.$inject = [
        '$scope',
        '$q',
        'TimesheetService',
        'NotificationService',
        'ValidationService'
    ];

    /**
     * Timesheet Controller
     * Handles timesheet entry and management functionality
     */
    function TimesheetController(
        $scope,
        $q,
        TimesheetService,
        NotificationService,
        ValidationService
    ) {
        var vm = this;

        // Public properties
        vm.timesheets = [];
        vm.selectedDate = new Date();
        vm.isLoading = false;
        vm.errors = {};

        // Public methods
        vm.loadTimesheets = loadTimesheets;
        vm.saveTimesheet = saveTimesheet;
        vm.deleteTimesheet = deleteTimesheet;
        vm.validateHours = validateHours;

        // Initialize
        activate();

        ////////////////

        /**
         * Initialize controller
         */
        function activate() {
            loadTimesheets();
            setupWatchers();
        }

        /**
         * Load timesheets for selected date
         */
        function loadTimesheets() {
            vm.isLoading = true;
            vm.errors = {};

            return TimesheetService
                .getTimesheets(vm.selectedDate)
                .then(function(response) {
                    vm.timesheets = response.data;
                    return vm.timesheets;
                })
                .catch(function(error) {
                    vm.errors.load = 'Failed to load timesheets';
                    NotificationService.error('Failed to load timesheets');
                    return $q.reject(error);
                })
                .finally(function() {
                    vm.isLoading = false;
                });
        }

        /**
         * Save timesheet entry
         */
        function saveTimesheet(timesheet) {
            if (!validateTimesheet(timesheet)) {
                return $q.reject('Validation failed');
            }

            vm.isLoading = true;

            var promise = timesheet.id 
                ? TimesheetService.updateTimesheet(timesheet)
                : TimesheetService.createTimesheet(timesheet);

            return promise
                .then(function(response) {
                    NotificationService.success('Timesheet saved successfully');
                    return loadTimesheets();
                })
                .catch(function(error) {
                    handleSaveError(error);
                    return $q.reject(error);
                })
                .finally(function() {
                    vm.isLoading = false;
                });
        }

        /**
         * Validate timesheet entry
         */
        function validateTimesheet(timesheet) {
            vm.errors = {};

            if (!timesheet.project_id) {
                vm.errors.project = 'Project is required';
            }

            if (!timesheet.spent_hours || timesheet.spent_hours <= 0) {
                vm.errors.hours = 'Hours must be greater than 0';
            }

            if (timesheet.spent_hours > 24) {
                vm.errors.hours = 'Hours cannot exceed 24 per day';
            }

            return Object.keys(vm.errors).length === 0;
        }

        /**
         * Setup watchers
         */
        function setupWatchers() {
            $scope.$watch('vm.selectedDate', function(newDate, oldDate) {
                if (newDate !== oldDate) {
                    loadTimesheets();
                }
            });
        }

        /**
         * Handle save errors
         */
        function handleSaveError(error) {
            if (error.status === 422 && error.data.errors) {
                vm.errors = error.data.errors;
            } else {
                vm.errors.save = 'Failed to save timesheet';
                NotificationService.error('Failed to save timesheet');
            }
        }
    }
})();

Service Standards

// app/modules/timesheet/services/TimesheetService.js

(function() {
    'use strict';

    angular
        .module('timesheet')
        .factory('TimesheetService', TimesheetService);

    TimesheetService.$inject = ['$http', '$q', 'ApiConfig'];

    /**
     * Timesheet Service
     * Handles all timesheet-related API calls
     */
    function TimesheetService($http, $q, ApiConfig) {
        var service = {
            getTimesheets: getTimesheets,
            createTimesheet: createTimesheet,
            updateTimesheet: updateTimesheet,
            deleteTimesheet: deleteTimesheet,
            approveTimesheet: approveTimesheet,
            getTimesheetSummary: getTimesheetSummary
        };

        return service;

        ////////////////

        /**
         * Get timesheets for a specific date range
         */
        function getTimesheets(startDate, endDate) {
            var params = {
                start_date: formatDate(startDate),
                end_date: formatDate(endDate || startDate)
            };

            return $http
                .get(ApiConfig.baseUrl + '/api/timesheets', { params: params })
                .then(handleSuccess)
                .catch(handleError);
        }

        /**
         * Create new timesheet entry
         */
        function createTimesheet(timesheet) {
            return $http
                .post(ApiConfig.baseUrl + '/api/timesheets', timesheet)
                .then(handleSuccess)
                .catch(handleError);
        }

        /**
         * Update existing timesheet entry
         */
        function updateTimesheet(timesheet) {
            return $http
                .put(ApiConfig.baseUrl + '/api/timesheets/' + timesheet.id, timesheet)
                .then(handleSuccess)
                .catch(handleError);
        }

        /**
         * Delete timesheet entry
         */
        function deleteTimesheet(timesheetId) {
            return $http
                .delete(ApiConfig.baseUrl + '/api/timesheets/' + timesheetId)
                .then(handleSuccess)
                .catch(handleError);
        }

        /**
         * Handle successful API responses
         */
        function handleSuccess(response) {
            return response.data;
        }

        /**
         * Handle API errors
         */
        function handleError(error) {
            console.error('TimesheetService error:', error);
            return $q.reject(error);
        }

        /**
         * Format date for API
         */
        function formatDate(date) {
            if (!date) return null;

            var d = new Date(date);
            return d.getFullYear() + '-' + 
                   String(d.getMonth() + 1).padStart(2, '0') + '-' + 
                   String(d.getDate()).padStart(2, '0');
        }
    }
})();

Testing Standards

Unit Test Standards

<?php

namespace Tests\Unit\Services;

use App\Models\User;
use App\Models\Project;
use App\Services\TimesheetCalculationService;
use App\Repositories\UserRepository;
use App\Repositories\ProjectRepository;
use Carbon\Carbon;
use Tests\TestCase;
use Mockery;

class TimesheetCalculationServiceTest extends TestCase
{
    private TimesheetCalculationService $service;
    private UserRepository $userRepository;
    private ProjectRepository $projectRepository;

    protected function setUp(): void
    {
        parent::setUp();

        $this->userRepository = Mockery::mock(UserRepository::class);
        $this->projectRepository = Mockery::mock(ProjectRepository::class);

        $this->service = new TimesheetCalculationService(
            $this->userRepository,
            $this->projectRepository
        );
    }

    /** @test */
    public function it_calculates_utilization_rate_correctly()
    {
        // Arrange
        $user = User::factory()->make(['id' => 1]);
        $startDate = Carbon::parse('2024-01-01');
        $endDate = Carbon::parse('2024-01-05'); // 5 working days

        $user->shouldReceive('timelogs->whereBetween->sum')
             ->once()
             ->andReturn(32.0); // 32 hours worked

        // Act
        $utilizationRate = $this->service->calculateUtilizationRate(
            $user,
            $startDate,
            $endDate
        );

        // Assert
        $this->assertEquals(80.0, $utilizationRate); // 32/40 * 100 = 80%
    }

    /** @test */
    public function it_throws_exception_when_no_working_days()
    {
        // Arrange
        $user = User::factory()->make(['id' => 1]);
        $startDate = Carbon::parse('2024-01-06'); // Saturday
        $endDate = Carbon::parse('2024-01-07');   // Sunday

        // Act & Assert
        $this->expectException(TimesheetException::class);
        $this->expectExceptionMessage('No working days in the specified period');

        $this->service->calculateUtilizationRate($user, $startDate, $endDate);
    }
}

Code Review Guidelines

Review Checklist

  • [ ] Code follows PSR-12 standards
  • [ ] Functions and classes have single responsibility
  • [ ] Code is properly documented
  • [ ] Tests are included and pass
  • [ ] Security considerations addressed
  • [ ] Performance implications considered
  • [ ] Error handling implemented
  • [ ] Database queries optimized
  • [ ] No hardcoded values
  • [ ] Consistent naming conventions

Review Process

  1. Automated Checks: Code must pass all automated checks (PHPStan, ESLint, tests)
  2. Peer Review: At least one team member must review and approve
  3. Security Review: Security-sensitive changes require security team review
  4. Performance Review: Performance-critical changes require performance review

This comprehensive coding standards document ensures consistent, maintainable, and high-quality code across the Stratpoint Timesheet Application.