'Urlop wypoczynkowy', self::TYPE_SICK => 'Chorobowe', self::TYPE_OTHER => 'Inna nieobecność' ]; const DEFAULT_DAYS_LIMIT = 26; private static bool $tables_checked = false; public function __construct( $mdb ) { $this -> mdb = $mdb; if ( !self::$tables_checked ) { $this -> ensureTables(); self::$tables_checked = true; } } private function ensureTables(): void { $pdo = $this -> mdb -> pdo; $pdo -> exec( " CREATE TABLE IF NOT EXISTS `users_vacations` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL, `date_from` DATE NOT NULL, `date_to` DATE NOT NULL, `type` VARCHAR(30) NOT NULL DEFAULT 'urlop_wypoczynkowy', `comment` TEXT, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_user_year` (`user_id`, `date_from`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; " ); $pdo -> exec( " CREATE TABLE IF NOT EXISTS `users_vacation_limits` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL, `year` SMALLINT UNSIGNED NOT NULL, `days_limit` TINYINT UNSIGNED NOT NULL DEFAULT 26, UNIQUE KEY `uq_user_year` (`user_id`, `year`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; " ); } public function allByYear( int $year, ?int $user_id = null ): array { $where = [ 'AND' => [ 'date_from[>=]' => $year . '-01-01', 'date_from[<=]' => $year . '-12-31' ], 'ORDER' => [ 'date_from' => 'DESC' ] ]; if ( $user_id ) $where['AND']['user_id'] = $user_id; return $this -> mdb -> select( 'users_vacations', [ '[>]users' => [ 'user_id' => 'id' ] ], [ 'users_vacations.id', 'users_vacations.user_id', 'users_vacations.date_from', 'users_vacations.date_to', 'users_vacations.type', 'users_vacations.comment', 'users.name', 'users.surname' ], $where ); } public function add( int $user_id, string $date_from, string $date_to, string $type, string $comment = '' ) { if ( !isset( self::TYPES[ $type ] ) ) return false; if ( $date_from > $date_to ) return false; $this -> mdb -> insert( 'users_vacations', [ 'user_id' => $user_id, 'date_from' => $date_from, 'date_to' => $date_to, 'type' => $type, 'comment' => $comment ] ); return $this -> mdb -> id(); } public function getById( int $id ): ?array { $row = $this -> mdb -> get( 'users_vacations', '*', [ 'id' => $id ] ); return $row ?: null; } public function update( int $id, int $user_id, string $date_from, string $date_to, string $type, string $comment = '' ): bool { if ( !isset( self::TYPES[ $type ] ) ) return false; if ( $date_from > $date_to ) return false; $this -> mdb -> update( 'users_vacations', [ 'user_id' => $user_id, 'date_from' => $date_from, 'date_to' => $date_to, 'type' => $type, 'comment' => $comment ], [ 'id' => $id ] ); return true; } public function delete( int $id ): bool { $this -> mdb -> delete( 'users_vacations', [ 'id' => $id ] ); return true; } public function getLimit( int $user_id, int $year ): int { $row = $this -> mdb -> get( 'users_vacation_limits', 'days_limit', [ 'user_id' => $user_id, 'year' => $year ] ); return $row !== null ? (int) $row : self::DEFAULT_DAYS_LIMIT; } public function setLimit( int $user_id, int $year, int $days_limit ): bool { $exists = $this -> mdb -> has( 'users_vacation_limits', [ 'user_id' => $user_id, 'year' => $year ] ); if ( $exists ) { $this -> mdb -> update( 'users_vacation_limits', [ 'days_limit' => $days_limit ], [ 'user_id' => $user_id, 'year' => $year ] ); } else { $this -> mdb -> insert( 'users_vacation_limits', [ 'user_id' => $user_id, 'year' => $year, 'days_limit' => $days_limit ] ); } return true; } public function getLimitsForYear( int $year ): array { $rows = $this -> mdb -> select( 'users_vacation_limits', '*', [ 'year' => $year ] ); $limits = []; foreach ( $rows as $row ) $limits[ (int) $row['user_id'] ] = (int) $row['days_limit']; return $limits; } public static function countBusinessDays( string $date_from, string $date_to ): int { $start = new \DateTime( $date_from ); $end = new \DateTime( $date_to ); $years = range( (int) $start -> format( 'Y' ), (int) $end -> format( 'Y' ) ); $holidays = self::getPolishHolidays( $years ); $days = 0; while ( $start <= $end ) { $dow = (int) $start -> format( 'N' ); if ( $dow <= 5 && !isset( $holidays[ $start -> format( 'Y-m-d' ) ] ) ) $days++; $start -> modify( '+1 day' ); } return $days; } private static function getPolishHolidays( array $years ): array { $holidays = []; foreach ( $years as $y ) { // Stałe święta ustawowe $fixed = [ "$y-01-01", // Nowy Rok "$y-01-06", // Trzech Króli "$y-05-01", // Święto Pracy "$y-05-03", // Święto Konstytucji "$y-08-15", // Wniebowzięcie NMP "$y-11-01", // Wszystkich Świętych "$y-11-11", // Święto Niepodległości "$y-12-25", // Boże Narodzenie "$y-12-26", // Drugi dzień Bożego Narodzenia ]; foreach ( $fixed as $d ) $holidays[ $d ] = true; // Wielkanoc i święta ruchome $easter = new \DateTime( date( 'Y-m-d', easter_date( $y ) ) ); $easter_monday = ( clone $easter ) -> modify( '+1 day' ); $corpus_christi = ( clone $easter ) -> modify( '+60 days' ); $holidays[ $easter -> format( 'Y-m-d' ) ] = true; $holidays[ $easter_monday -> format( 'Y-m-d' ) ] = true; $holidays[ $corpus_christi -> format( 'Y-m-d' ) ] = true; } return $holidays; } public function usedDaysByYear( int $year, ?string $type = null ): array { $where = [ 'AND' => [ 'date_from[>=]' => $year . '-01-01', 'date_from[<=]' => $year . '-12-31' ] ]; if ( $type ) $where['AND']['type'] = $type; $rows = $this -> mdb -> select( 'users_vacations', [ 'user_id', 'date_from', 'date_to' ], $where ); $totals = []; foreach ( $rows as $row ) { $uid = (int) $row['user_id']; if ( !isset( $totals[ $uid ] ) ) $totals[ $uid ] = 0; $totals[ $uid ] += self::countBusinessDays( $row['date_from'], $row['date_to'] ); } return $totals; } public function summaryByYear( int $year, array $users ): array { $limits = $this -> getLimitsForYear( $year ); $used_map = $this -> usedDaysByYear( $year, self::TYPE_VACATION ); $summary = []; foreach ( $users as $u ) { $uid = (int) $u['id']; $limit = $limits[ $uid ] ?? self::DEFAULT_DAYS_LIMIT; $used = $used_map[ $uid ] ?? 0; $summary[] = [ 'user_id' => $uid, 'name' => $u['name'] . ' ' . $u['surname'], 'limit' => $limit, 'used' => $used, 'remaining' => $limit - $used ]; } return $summary; } public function carryoverByYear( int $year, array $users ): array { // Znajdź najwcześniejszy rok z danymi (urlopy lub limity) $min_vacation_year = $this -> mdb -> get( 'users_vacations', 'date_from', [ 'ORDER' => [ 'date_from' => 'ASC' ], 'LIMIT' => 1 ] ); $min_limit_year = $this -> mdb -> get( 'users_vacation_limits', 'year', [ 'ORDER' => [ 'year' => 'ASC' ], 'LIMIT' => 1 ] ); $start_year = $year - 1; // domyślnie sprawdzaj przynajmniej rok wstecz if ( $min_vacation_year ) $start_year = min( $start_year, (int) substr( $min_vacation_year, 0, 4 ) ); if ( $min_limit_year ) $start_year = min( $start_year, (int) $min_limit_year ); if ( $start_year >= $year ) return []; $carryover = []; foreach ( $users as $u ) { $uid = (int) $u['id']; $total_remaining = 0; $year_details = []; for ( $y = $start_year; $y < $year; $y++ ) { $limit = $this -> getLimit( $uid, $y ); $used_map = $this -> usedDaysByYear( $y, self::TYPE_VACATION ); $used = $used_map[ $uid ] ?? 0; $remaining = $limit - $used; if ( $remaining > 0 ) { $total_remaining += $remaining; $year_details[] = [ 'year' => $y, 'remaining' => $remaining ]; } } if ( $total_remaining > 0 ) { $carryover[] = [ 'user_id' => $uid, 'name' => $u['name'] . ' ' . $u['surname'], 'total' => $total_remaining, 'years' => $year_details ]; } } return $carryover; } }