Search by

kawayanmedia / dtr-pdf

mannysoft

Framework-agnostic Daily Time Record (DTR) computation and PDF generator

Package info

github.com/kawayanmedia/dtr-pdf

pkg:composer/kawayanmedia/dtr-pdf

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-11-06 06:24 UTC

This package is auto-updated.

Last update: 2026-11-08 05:48:55 UTC


README

Framework-agnostic Daily Time Record (DTR) engine and PDF generator for Philippine government attendance forms.

It is a faithful port of the legacy iHRMIS DTR screen: the same tardiness computation, week buckets, 40-hour rule, OB/TO/leave/holiday labels and the same FPDI drawing coordinates. Database access, HTTP handling and side-effect writes are injected through small interfaces, so the package works in CodeIgniter, Laravel, Symfony or plain PHP.

Requirements

  • PHP 8.2+
  • setasign/fpdi ^2.6 and setasign/fpdf ^1.9 (installed automatically)
  • A DTR template PDF — one is bundled at resources/templates/dtr.pdf, and DtrLayout::legacyDtr() matches it exactly

Installation

composer require kawayanmedia/dtr-pdf

While the package lives inside the iHRMIS repository, install it through a path repository:

{
    "repositories": [
        { "type": "path", "url": "packages/dtr-pdf", "options": { "symlink": true } }
    ],
    "require": {
        "kawayanmedia/dtr-pdf": "@dev"
    }
}

kawayanmedia/persistence comes with it as a dependency; it is only used by the write side, so printing needs no database at all.

See it running

The package ships with a database-free example that renders two employees (regular and overnight shifts) onto the iHRMIS template:

php packages/dtr-pdf/examples/generate-sample.php
open packages/dtr-pdf/examples/output/dtr-sample.pdf

The script writes examples/output/dtr-sample.pdf and prints the computed totals. examples/output/ is git-ignored, so generated files never get committed. Use examples/generate-sample.php as a starting point for your own integration.

The script uses the bundled template, so it runs from a plain checkout. When the package is installed with Composer it lives at vendor/kawayanmedia/dtr-pdf/examples/generate-sample.php; pass your own template and output paths as arguments to override the defaults:

php vendor/kawayanmedia/dtr-pdf/examples/generate-sample.php \
    /path/to/dtr.pdf /tmp/dtr-sample.pdf

The write side has its own runnable example, which creates a SQLite schema and records logs, tardiness and an undertime posting:

php packages/dtr-pdf/examples/write-sample.php

Quick start

use KawayanMedia\DtrPdf\Adapter\ArrayHolidays;
use KawayanMedia\DtrPdf\Adapter\ArrayHistory;
use KawayanMedia\DtrPdf\Adapter\ArrayLeaveTypes;
use KawayanMedia\DtrPdf\Adapter\ArrayManualLogs;
use KawayanMedia\DtrPdf\Adapter\ArraySchedules;
use KawayanMedia\DtrPdf\Adapter\ArraySettings;
use KawayanMedia\DtrPdf\Adapter\ArraySignatories;
use KawayanMedia\DtrPdf\Adapter\NullTardinessStore;
use KawayanMedia\DtrPdf\Config\DtrLayout;
use KawayanMedia\DtrPdf\DtrGenerator;
use KawayanMedia\DtrPdf\Engine\DtrEngine;
use KawayanMedia\DtrPdf\Model\Employee;
use KawayanMedia\DtrPdf\Model\Period;
use KawayanMedia\DtrPdf\Model\RawDtrDay;
use KawayanMedia\DtrPdf\Renderer\DtrPdfRenderer;

$engine = new DtrEngine(
    schedules: new ArraySchedules([
        1 => ['time_a' => '08:00', 'time_b' => '12:00', 'time_c' => '13:00', 'time_d' => '17:00'],
    ]),
    holidays: new ArrayHolidays(),
    leaveTypes: new ArrayLeaveTypes([1 => ['name' => 'Vacation Leave', 'code' => 'VL']]),
    manualLogs: new ArrayManualLogs(),
    history: new ArrayHistory(),
    tardiness: new NullTardinessStore(),   // or your repository
    settings: new ArraySettings(['allow_forty_hours' => '1']),
    signatories: new ArraySignatories(['013001' => ['name' => 'Maria Santos', 'position' => 'HR Officer']]),
);

$generator = new DtrGenerator(
    $engine,
    new DtrPdfRenderer(DtrLayout::legacyDtr('/absolute/path/dtr/template/dtr.pdf')),
);

$employee = new Employee(
    id: '013001',
    firstName: 'Juan',
    middleName: 'Santos',
    lastName: 'Dela Cruz',
    shiftType: 1,
);

$report = $generator->report(
    $employee,
    Period::monthly(2026, 11, 1, 15),
    [
        new RawDtrDay('2026-11-02', '08:00', '12:00', '13:00', '17:00'),
        new RawDtrDay('2026-11-03', '08:30', '12:00', '13:10', '17:00'),
    ],
);

$bytes = $generator->renderer()->render($report); // PDF string
$generator->renderer()->save($report, '/tmp/dtr.pdf');
$generator->renderer()->stream([$report], 'dtr.pdf'); // multi-report, one page each

Arrays shaped like the legacy employee_dtr() rows are accepted too:

new RawDtrDay(...);                  // preferred
RawDtrDay::fromArray($legacyRow);    // [log_date, am_login, am_logout, pm_login, …]

Architecture

RawDtrDay[] ──► DtrEngine ──► TardinessCalculator ──► TardinessState
                    │                 │
                    │                 ├── RegularShiftCalculator  (shift 1 / 3)
                    │                 └── ShiftingCalculator      (shift 2 / 4)
                    │
                    ├── DtrPresenter ──► PresentedDay[] (labels, colours, 12-hour text)
                    └── RunningTotals ──► DtrTotals (40-hour rule, weeks)

DtrReport ──► DtrPdfRenderer ──► FPDI + your template ──► %PDF
Legacy Package
Attendance::dtr() drawing Renderer\DtrPdfRenderer
Attendance::regular_hours() Engine\RegularShiftCalculator
Attendance::shifting_same_day() Engine\ShiftingCalculator
Helps::check_late*, check_undertime* Engine\TardinessCalculator
Helps::compute_time Support\DurationFormatter
DTR label rules Engine\DtrPresenter
Dtr, Tardiness, Holiday, Leave_type, Manual_log, Shift, Schedule_employees, Setting, Office contracts in Contract/

Contracts to implement

Interface Legacy source
ScheduleProviderInterface Shift::shift_times(), Schedule_employees::get_schedule()
HolidayProviderInterface Holiday::is_holiday(), half_day, am_pm, holiday_name()
LeaveTypeProviderInterface Leave_type::get_leave_name(), get_leave_code()
ManualLogProviderInterface Manual_log::get_notes()
DtrHistoryProviderInterface Dtr::date_tom()
TardinessStoreInterface Tardiness::delete_tardiness(), check_tardiness()
SettingsInterface Setting::getField()
SignatoryProviderInterface Office / Office_m office-head lookup
LeaveConversionProviderInterface Leave_conversion_table::compute_hour_minute()

The package calls TardinessStoreInterface in the same order as the legacy controller, so existing tardiness reports remain identical.

Configuration

Settings read through SettingsInterface

Key Meaning
show_employee_number_dtr yes prints the employee id on both copies
last_name_first_dtr yes prints LAST, FIRST MIDDLE
print_overtime_in_dtr 1 prints the overtime line
print_office_head_in_dtr 1 prints the signatory block
allow_forty_hours 1 enables the 40-hour weekly offset (shift_type 2/4)
lgu_code marinduque_province selects the combined late/undertime total
undertime_tardi 1 colours undertime red and counts it
minutes_tardy grace period in minutes (legacy default 1 = no grace)
minutes_tardy_am_only 1 applies the grace only in the morning
dont_compute comma-separated dates excluded from tardiness
tardy_autodeduct yes marks the computed leave-card undertime as active

Profile

DtrProfile::legacyIhrmis() reproduces the controller's hard-coded exceptions (employee 152 is treated as shift type 1). DtrProfile::default() has none. DtrProfile::$preserveLastShiftWindow keeps the previous shift window when the schedule repository has no row for a day, matching the legacy stateful behaviour.

Layout

Every coordinate, font size and colour lives in DtrLayout. The DtrLayout::legacyDtr() preset matches dtr/template/dtr.pdf, including both copies per page. Override individual values (for example a Letter-sized template or a different totals position):

$layout = new DtrLayout(
    templatePath: '/path/to/dtr.pdf',
    rowSpacing: 3.6,
    totalsAnchorY: 36,
    signatoryY: 219,
);

compress: false disables PDF stream compression (useful for text assertions in tests).

Leave-card undertime

The value computed by the legacy leave_card_undertime() is available on every report, so it can be used later (leave-card import, payroll deduction, audit):

$report = $generator->report($employee, $period, $rows);

// Or skip building the report in your code:
// $undertime = $generator->leaveCardUndertime($employee, $period, $rows);

/** @var \KawayanMedia\DtrPdf\Model\LeaveCardUndertime $undertime */
$undertime = $report->leaveCardUndertime;

$undertime->action;       // "Nov. 2026 Undertime"
$undertime->particulars;  // "UT-0-8-35"
$undertime->days;         // 0
$undertime->hours;        // 8
$undertime->minutes;      // 35
$undertime->value;        // "1.000" (leave-credit equivalent, 3 decimals)
$undertime->lastDay;      // "2026-11-30"
$undertime->active;       // true when tardy_autodeduct = yes
$undertime->isZero();     // true when particulars is "UT-0-0-0"
$undertime->hasValue();   // !isZero()

The minutes lookup comes from LeaveConversionProviderInterface (the legacy leave_conversion_table). It is optional and defaults to zero:

use KawayanMedia\DtrPdf\Adapter\ArrayLeaveConversions;

$engine = new DtrEngine(
    // …
    leaveConversions: new ArrayLeaveConversions([
        0 => 0.0,
        15 => 0.031,
        30 => 0.063,
    ]),
);

The package writes the leave card itself. Hand the computed value to Service\DtrRecorder::writeLeaveCard(), which performs the legacy is_entry_exists() / insert_entry() / update_entry() decision and removes the month's entry when the undertime is zero (delete_undertime()):

use KawayanMedia\DtrPdf\Adapter\PdoDtrStore;
use KawayanMedia\DtrPdf\Service\DtrRecorder;
use KawayanMedia\Persistence\Adapter\PdoDatabase;

$recorder = new DtrRecorder(
    new PdoDtrStore(PdoDatabase::fromDsn('mysql:host=localhost;dbname=ihrmis', $user, $pass)),
);

$recorder->writeLeaveCard($undertime);   // insert, update or delete

The plain PHP and Laravel guides show the provider and the recorder call.

Behaviour notes preserved from the legacy code

  • The period always computes from day 1; days before the visible from day are blanked and excluded from tardiness.
  • Week buckets: 01–07, 08–14, 15–21, 22–31.
  • The 40-hour rule compares the formatted week duration with '5 days' exactly as the legacy code did, then subtracts that week's late/undertime counts.
  • shift_type 1 and 3 use RegularShiftCalculator; 2 and 4 use ShiftingCalculator (day, overnight and 24-hour windows).
  • Overnight and 24-hour branches read the next day through DtrHistoryProviderInterface.
  • The renderer returns bytes or writes files and never sends HTTP headers or touches sessions. Attendance writes go through Service\DtrRecorder, including the leave-card posting; archiving and concatenation stay with the host application.

Writing data

Service\DtrRecorder is the write side: blank-row creation and clean-up, manual logs, cell edits, tardiness accrual and the monthly undertime posting.

use KawayanMedia\DtrPdf\Adapter\PdoDtrStore;
use KawayanMedia\DtrPdf\Service\DtrRecorder;
use KawayanMedia\Persistence\Adapter\PdoDatabase;

$store    = new PdoDtrStore(PdoDatabase::fromDsn('mysql:host=localhost;dbname=ihrmis', $user, $pass));
$recorder = new DtrRecorder($store);

$created = $recorder->ensureDates($employeeId, '2026-01-01', '2026-01-31', $officeId);
$recorder->removeDates($employeeId, $created);

$recorder->log($employeeId, '2026-01-05', 1, '8:00', $officeId);
$recorder->updateRow($rowId, ['pm_logout' => '5:45']);
$recorder->recordTardiness($employeeId, '2026-01-05', 1, 300);
$recorder->postUndertime($employeeId, 1, 2026, 3600);

// … or write the value already computed by a report:
$recorder->writeLeaveCard($report->leaveCardUndertime);

Log types 3–6 normalise afternoon times ("1:30" → "13:30") and accept the legacy o:b marker. Tables, columns, the DtrStoreInterface seam and the behaviour differences are documented in docs/writing-data.md.

Using it in another project

Stack Guide
Plain PHP (PDO, no framework) docs/plain-php.md
Laravel (service provider, query builder, controller, command) docs/laravel.md
CodeIgniter see below

Both guides include complete provider implementations, saving/streaming the PDF and handling multiple employees.

CodeIgniter

The package has no framework dependency, so inside a controller:

$engine = new \KawayanMedia\DtrPdf\Engine\DtrEngine(
    schedules: new MyScheduleRepository(),
    // …
);

$generator = new \KawayanMedia\DtrPdf\DtrGenerator(
    $engine,
    new \KawayanMedia\DtrPdf\Renderer\DtrPdfRenderer(
        \KawayanMedia\DtrPdf\Config\DtrLayout::legacyDtr(FCPATH . 'dtr/template/dtr.pdf'),
    ),
);

return $this->response->download('dtr.pdf', $generator->render($employee, $period, $rows));

For CodeIgniter 2/3, enable $config['composer_autoload'] = TRUE; (or require vendor/autoload.php) and use the same classes.

Testing

vendor/bin/phpunit -c packages/dtr-pdf/phpunit.xml.dist

The suite covers the formatter, calendar, tardiness rules, both shift calculators, the presenter, totals, engine orchestration and real PDF output. LegacyParityTest compares DurationFormatter with the legacy Helps::compute_time() whenever the iHRMIS app/ tree is available.