PHP Classes

File: src/api_handlers.php

Recommend this page to a friend!
  Packages of Uku-Kaarel Jo~esaar   too-vark   src/api_handlers.php   Download  
File: src/api_handlers.php
Role: Application script
Content type: text/plain
Description: Application script
Class: too-vark
Manage people work schedule times and tasks
Author: By
Last change:
Date: 9 days ago
Size: 18,389 bytes
 

Contents

Class file image Download
<?php // ??? API HANDLER FUNCTIONS // PDO + params, return [int $code, array $body]. // Called by api.php router AND by tests/run.php directly. // Depends on helpers.php (validate_task, upsert_task, statusLabels, btnLabels, timeShift, __). /** Worker's today view ? includes yesterday's unfinished. */ function api_tasks_today(PDO $pdo, int $uid, DateContext $dc): array { $today_date = $dc->today; $current_time = $dc->time; $yesterday = $dc->yesterday(); // $stmt=$pdo->prepare("SELECT * FROM tasks WHERE user_id=? AND (task_date=? OR task_date=?) ORDER BY title"); $stmt=$pdo->prepare("SELECT t.*, d.description FROM tasks t LEFT JOIN task_details d ON t.title=d.title WHERE user_id=? AND (task_date=? OR task_date=?) ORDER BY t.title"); $stmt->execute([$uid,$yesterday, $today_date]); $btn=btnLabels(); $coworkers = coworkers_map($pdo, $uid, "(t.task_date=? OR t.task_date=?)", [$yesterday, $today_date]); $out=[]; foreach ($stmt->fetchAll() as $r) { if ($r['status'] == 2 && $r['task_date'] != $today_date) continue; [$start, $end]=timeShift($r['start_time'], $r['end_time']); if ($r['status'] == '1') $end=$current_time; elseif ($r['status'] == '2') $end=$r['end_time']; $key=$r['task_date']."\t".$r['title']; $out[]=[ 'id' => (int)$r['id'], 'title' => $r['title'], 'start_time' => $r['status'] == '0' ? $start : $r['start_time'], 'end_time' => $end, 'status' => (int)$r['status'], 'status_text'=> $btn[(int)$r['status']], 'notes' => (string)$r['notes'], 'description' => $r['description'], 'coworkers' => $coworkers[$key] ?? [] ]; } return [200, $out]; } /** Month tasks + rules + meta for one user. */ function api_tasks_month(PDO $pdo, int $uid, bool $is_admin, string $ym, int $target_uid, DateContext $dc): array { $today_date = $dc->today; $stmt=$pdo->prepare("SELECT rules_text FROM user_rules WHERE user_id=?"); $stmt->execute([$target_uid]); $rules_text=$stmt->fetchColumn() ?: ""; $status_texts=statusLabels(); $grouped=[]; $ym_first = $ym . '-01'; $prev_first = date('Y-m-d', strtotime($ym_first . ' -1 month')); $next_first = date('Y-m-d', strtotime($ym_first . ' +1 month')); // Single query: fetch both last month and current month, split in PHP //$t=hrtime(true);//DB_TIME $stmt=$pdo->prepare("SELECT id, user_id, title, start_time, end_time, status, task_date, notes FROM tasks WHERE user_id=? AND task_date >= ? AND task_date < ? ORDER BY task_date, start_time, title"); $stmt->execute([$target_uid, $prev_first, $next_first]); $all_rows=$stmt->fetchAll(); //if(function_exists('timer_log'))timer_log(['qs'=>'SQL:tasks_month','jstm'=>0,'fetch'=>round((hrtime(true)-$t)/1e6,2),'render'=>0]);//DB_TIME //$t=hrtime(true);//DB_TIME $coworkers = coworkers_map($pdo, $target_uid, "t.task_date >= ? AND t.task_date < ?", [$ym_first, $next_first]); //if(function_exists('timer_log'))timer_log(['qs'=>'SQL:coworkers_month','jstm'=>0,'fetch'=>round((hrtime(true)-$t)/1e6,2),'render'=>0]);//DB_TIME $last_month_data = []; foreach ($all_rows as $row) { if ($row['task_date'] < $ym_first) { $last_month_data[] = array_values([ $row['title'] ,$row['task_date'] ,$row['start_time'] ,$row['end_time'] ,statusLabels()[$row['status']] ,$row['notes']]); //$last_month_data[] = array_values([$row['id $row['user_id'],$row['title']]); continue; } $row['status_text']=$status_texts[(int)$row['status']] ?? '?'; $row['username']=''; $key=$row['task_date']."\t".$row['title']; $row['coworkers']=$coworkers[$key] ?? []; $grouped[$row['task_date']][]=$row; } $workers=$is_admin ? $pdo->query("SELECT id, username FROM users ORDER BY username")->fetchAll() : []; return [200, [ 'target_uid' => $target_uid, 'rules_text' => $rules_text, 'grouped' => $grouped, 'ym' => $ym, 'prev_ym' => substr($prev_first, 0, 7), 'next_ym' => substr($next_first, 0, 7), 'today' => $today_date, 'last_month_data'=> $last_month_data, 'workers' => $workers, 'known_titles' => known_titles($pdo), 'full_weeks' => full_weeks_info($ym), 'is_admin' => $is_admin, ]]; } /** Admin team overview ? grouped by time (today) or date (month). */ function api_tasks_team(PDO $pdo, bool $is_admin, string $scope, DateContext $dc): array { $today_date = $dc->today; $current_month_first = $dc->month; if (!$is_admin) return [403, ['error' => 'forbidden']]; $is_month=($scope === 'month'); $status_texts=statusLabels(); $grouped=[]; //$t=hrtime(true);//DB_TIME if ($is_month) { $ym_first = $current_month_first; $next_first = date('Y-m-d', strtotime($ym_first . ' +1 month')); $stmt=$pdo->prepare("SELECT t.id, u.username, u.id as user_id, t.title, t.start_time, t.end_time, t.status, t.notes, t.task_date FROM tasks t JOIN users u ON t.user_id=u.id WHERE t.task_date >= ? AND t.task_date < ? ORDER BY t.task_date, u.username"); $stmt->execute([$ym_first, $next_first]); } else { $stmt=$pdo->prepare("SELECT t.id, u.username, u.id as user_id, t.title, t.start_time, t.end_time, t.status, t.notes, t.task_date FROM tasks t JOIN users u ON t.user_id=u.id WHERE t.task_date = ? ORDER BY t.start_time, u.username"); $stmt->execute([$today_date]); } //if(function_exists('timer_log'))timer_log(['qs'=>'SQL:tasks_team','jstm'=>0,'fetch'=>round((hrtime(true)-$t)/1e6,2),'render'=>0]);//DB_TIME foreach ($stmt->fetchAll() as $row) { $key=$is_month ? $row['task_date'] : ($row['start_time'] ?: '???'); $row['status_text']=$status_texts[(int)$row['status']] ?? '?'; $grouped[$key][]=$row; } return [200, [ 'grouped' => $grouped, 'workers' => workers_list($pdo), 'known_titles' => known_titles($pdo), 'is_month' => $is_month, 'today' => $today_date, ]]; } /** Print data ? joins tasks with details. */ function api_tasks_print(PDO $pdo, int $uid, bool $is_admin, ?string $task_id, DateContext $dc): array { $today_date = $dc->today; //$t=hrtime(true);//DB_TIME $sql="SELECT u.username, u.real_name, u.contact AS user_contact, t.title, t.task_date, t.start_time, t.end_time, t.notes, d.address, d.description, d.related_person FROM tasks t JOIN users u ON t.user_id=u.id LEFT JOIN task_details d ON t.title=d.title WHERE "; $params=[]; if ($task_id) { $sql .= "t.id=?"; $params[]=$task_id; if (!$is_admin) { $sql .= " AND t.user_id=?"; $params[]=$uid; } } else { if (!$is_admin) return [403, ['error' => 'forbidden']]; $sql .= "t.task_date=?"; $params[]=$today_date; } $sql .= " ORDER BY u.username, t.start_time"; $stmt=$pdo->prepare($sql); $stmt->execute($params); $print_data=[]; foreach ($stmt->fetchAll() as $row) { $key = ($row['real_name'] ?: $row['username']); $print_data[$key][]=$row; } //if(function_exists('timer_log'))timer_log(['qs'=>'SQL:api_tasks_print','jstm'=>0,'fetch'=>round((hrtime(true)-$t)/1e6,2),'render'=>0]);//DB_TIME // Org config for print header (from global $cfg loaded at startup) global $cfg; $org = array_filter($cfg, fn($v, $k) => str_starts_with($k, 'org_'), ARRAY_FILTER_USE_BOTH); return [200, ['print_data' => $print_data, 'today' => $today_date, 'org' => $org]]; } /** Status progression: 0?1?2. */ function api_tasks_status(PDO $pdo, array $d, int $uid): array { if (empty($d['id'])) return [400, ['error' => 'id is required']]; if ($err = validate_task($d, false, false)) return [400, ['error' => $err]]; $new_status=min((int)$d['status'] + 1, 2); $stmt=$pdo->prepare("UPDATE tasks SET start_time=?, end_time=?, status=?, notes=? WHERE id=? AND user_id=?"); $stmt->execute([$d['start_time'], $d['end_time'], $new_status, $d['notes'] ?? '', $d['id'], $uid]); if ($stmt->rowCount() === 0) return [404, ['error' => 'not_found']]; $labels=btnLabels(); return [200, ['status' => (string)$new_status, 'start_time' => $d['start_time'], 'end_time' => $d['end_time'], 'msg' => $labels[$new_status]]]; } /** Create or update a single task. */ function api_tasks_save(PDO $pdo, array $d, int $uid, bool $is_admin): array { if ($err = validate_task($d)) return [400, ['error' => $err]]; $status=isset($d['status']) ? (int)$d['status'] : 0; return db_try('api_tasks_save', function() use ($pdo, $d, $uid, $is_admin, $status) { if (!empty($d['id'])) { $sql="UPDATE tasks SET title=?, task_date=?, start_time=?, end_time=?, status=?, notes=?"; $params=[$d['title'], $d['task_date'], $d['start_time'], $d['end_time'], $status, $d['notes'] ?? '']; if ($is_admin && !empty($d['user_id'])) { $sql .= ", user_id=?"; $params[]=(int)$d['user_id']; } $sql .= " WHERE id=?"; $params[]=$d['id']; if (!$is_admin) { $sql .= " AND user_id=?"; $params[]=$uid; } $pdo->prepare($sql)->execute($params); $new_id = (int)$d['id']; } else { $target_uid=$uid; if ($is_admin) { if (!empty($d['user_id'])) $target_uid=(int)$d['user_id']; elseif (!empty($d['worker_ids'])) $target_uid=is_array($d['worker_ids']) ? (int)$d['worker_ids'][0] : (int)$d['worker_ids']; } upsert_task($pdo, $target_uid, $d['title'], $d['task_date'], $d['start_time'], $d['end_time'], $status, $d['notes'] ?? ''); $stmt = $pdo->prepare("SELECT id FROM tasks WHERE user_id=? AND task_date=? AND title=?"); $stmt->execute([$target_uid, $d['task_date'], $d['title']]); $new_id = (int)$stmt->fetchColumn(); } return [200, ['msg' => 'ok', 'id' => $new_id]]; }, 'Worker already has a task with this title on this date.'); } /** Delete task with status<2 guard. */ function api_tasks_delete(PDO $pdo, array $d, int $uid, bool $is_admin): array { if (empty($d['id'])) return [400, ['error' => 'id is required']]; $sql="DELETE FROM tasks WHERE id=? AND status < 2"; $params=[$d['id']]; if (!$is_admin) { $sql .= " AND user_id=?"; $params[]=$uid; } $stmt=$pdo->prepare($sql); $stmt->execute($params); if ($stmt->rowCount() === 0) return [404, ['error' => 'not_found']]; return [200, ['msg' => 'ok']]; } /** Save rules + auto-cleanup + generate schedule. */ function api_rules_generate(PDO $pdo, array $d, int $target_uid, string $req_ym): array { $rules_json=trim($d['rules_txt'] ?? ''); $ym_first = $req_ym . '-01'; $next_first = date('Y-m-d', strtotime($ym_first . ' +1 month')); $rules=json_decode($rules_json, true); if (!is_array($rules)) return [400, ['error' => 'Invalid rules JSON']]; //$t=hrtime(true);//DB_TIME $pdo->beginTransaction(); try { $pdo->prepare("INSERT INTO user_rules (user_id, rules_text) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET rules_text=excluded.rules_text") ->execute([$target_uid, $rules_json]); $pdo->prepare("DELETE FROM tasks WHERE user_id=? AND task_date >= ? AND task_date < ? AND status=0 AND source='rule'") ->execute([$target_uid, $ym_first, $next_first]); $insert_stmt=$pdo->prepare("INSERT OR IGNORE INTO tasks (user_id, task_date, title, start_time, end_time, status, notes, source) VALUES (?, ?, ?, ?, ?, 0, '', 'rule')"); $y=(int)substr($req_ym, 0, 4); $m=(int)substr($req_ym, 5, 2); $days_in_month=(int)date('t', mktime(0, 0, 0, $m, 1, $y)); // Reuse shared full-week helper ? build ISO-to-relative mapping $fw_info = full_weeks_info($req_ym); $iso_to_rel = []; foreach ($fw_info as $rel => $wk) $iso_to_rel[$wk['iso']] = $rel; for ($day=1; $day <= $days_in_month; $day++) { $ts=mktime(12, 0, 0, $m, $day, $y); $day_num=(int)date('N', $ts); $iso_week=(int)date('W', $ts); $rel_week=$iso_to_rel[$iso_week] ?? 0; if ($rel_week === 0) continue; // partial week ? skip foreach ($rules as $r) { if (empty($r['title']) || empty($r['days']) || empty($r['weeks']) || empty($r['start']) || empty($r['end'])) continue; $day_col=strtr($r['days'], ['E'=>'1','T'=>'2','K'=>'3','N'=>'4','R'=>'5','L'=>'6','P'=>'7']); $match=(strpos($day_col, (string)$day_num) !== false) && (strpos((string)$r['weeks'], (string)$rel_week) !== false); if ($match) $insert_stmt->execute([$target_uid, date('Y-m-d', $ts), $r['title'], $r['start'], $r['end']]); } } $pdo->commit(); //if(function_exists('timer_log'))timer_log(['qs'=>'SQL:rules_gen','jstm'=>0,'fetch'=>round((hrtime(true)-$t)/1e6,2),'render'=>0]);//DB_TIME return [200, ['msg' => 'ok']]; } catch (Exception $e) { $pdo->rollBack(); error_log('api_rules_generate: ' . $e->getMessage()); return [500, ['error' => 'Schedule generation failed']]; } } /** All task details + known addresses/contacts. * Bundles users + config to save round-trips on initial admin page load. * Inline config edits use the lighter standalone ?api=config GET instead. */ function api_details_get(PDO $pdo): array { $all=$pdo->query("SELECT * FROM task_details ORDER BY title")->fetchAll(); $addresses = array_values(array_unique(array_filter(array_column($all, 'address')))); $contacts = array_values(array_unique(array_filter(array_column($all, 'related_person')))); $wal_status = $pdo->query("PRAGMA wal_checkpoint(PASSIVE);")->fetchAll(PDO::FETCH_ASSOC); if(array_sum($wal_status[0])==0) $db_status = "WAL OK"; else $db_status = "WAL checkpoint status: ". print_r($wal_status[0],true); $users = users_full_list($pdo); global $cfg; $config = array_map(fn($k,$v) => ['key'=>$k,'val'=>$v], array_keys($cfg), array_values($cfg)); return [200, [ 'details' => $all, 'known_addresses' => $addresses, 'known_contacts' => $contacts, 'db_status'=>$db_status, 'users'=>$users, 'config'=>$config ]]; } /** Batch-assign a task to multiple workers. Admin only (checked in router). */ function api_tasks_batch(PDO $pdo, array $d): array { if ($err = validate_task($d)) return [400, ['error' => $err]]; if (empty($d['worker_ids']) || !is_array($d['worker_ids'])) return [400, ['error' => 'worker_ids required']]; $pdo->beginTransaction(); $result = db_try('tasks/batch', function() use ($pdo, $d) { foreach ($d['worker_ids'] as $wid) { upsert_task($pdo, (int)$wid, $d['title'], $d['task_date'], $d['start_time'], $d['end_time'], 0, $d['notes'] ?? ''); } $pdo->commit(); return [200, ['msg' => 'ok', 'updated' => count($d['worker_ids'])]]; }); if ($result[0] !== 200 && $pdo->inTransaction()) $pdo->rollBack(); return $result; } /** List all users. Admin only (checked in router). */ function api_users_list(PDO $pdo): array { return [200, users_full_list($pdo)]; } /** Update an existing user. Admin only (checked in router). */ function api_users_update(PDO $pdo, array $d): array { $id = (int)($d['id'] ?? 0); if ($id <= 0) return [400, ['error' => 'id is required']]; if ($id === 1) return [403, ['error' => 'admin_protected']]; $username = trim($d['username'] ?? ''); if ($username === '') return [400, ['error' => 'username is required']]; return db_try('users/update', function() use ($pdo, $d, $id, $username) { $sql = "UPDATE users SET username=?, real_name=?, contact=?"; $params = [$username, trim($d['real_name'] ?? ''), trim($d['contact'] ?? '')]; if (!empty($d['password'])) { $sql .= ", password=?"; $params[] = password_hash($d['password'], PASSWORD_DEFAULT); } $sql .= " WHERE id=?"; $params[] = $id; $stmt = $pdo->prepare($sql); $stmt->execute($params); if ($stmt->rowCount() === 0) return [404, ['error' => 'not_found']]; return [200, ['msg' => 'ok', 'id' => $id, 'username' => $username, 'real_name' => trim($d['real_name'] ?? ''), 'contact' => trim($d['contact'] ?? '')]]; }, 'username_exists'); } /** Create a new user. Admin only (checked in router). */ function api_users_create(PDO $pdo, array $d): array { $a_u = trim($d['username'] ?? ''); $a_p = trim($d['password'] ?? ''); $a_rn = trim($d['real_name'] ?? ''); $a_c = trim($d['contact'] ?? ''); if ($a_u === '' || $a_p === '') return [400, ['error' => 'empty_credentials']]; return db_try('users', function() use ($pdo, $a_u, $a_p, $a_rn, $a_c) { if (insert_user($pdo, $a_u, $a_p,$a_rn,$a_c) === 0) return [400, ['error' => 'user_exists']]; $new_id = (int)$pdo->lastInsertId(); return [200, ['msg' => 'ok', 'id' => $new_id, 'username' => $a_u, 'real_name' => $a_rn, 'contact' => $a_c]]; }); } /** Delete a user by id. Admin protected (id<=1). Admin only (checked in router). */ function api_users_delete(PDO $pdo, array $d): array { $id = (int)($d['id'] ?? 0); if ($id <= 1) return [403, ['error' => 'admin_protected']]; return db_try('users/delete', function() use ($pdo, $id) { $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$id]); return $stmt->rowCount() === 0 ? [404, ['error' => 'not_found']] : [200, ['msg' => 'ok']]; }); } /** Change own password. Verifies old password first. */ function api_users_password(PDO $pdo, array $d, int $uid): array { if (empty($d['new_password'])) return [400, ['error' => 'empty_password']]; if (empty($d['old_password'])) return [400, ['error' => 'old_password_required']]; $stmt = $pdo->prepare("SELECT password FROM users WHERE id = ?"); $stmt->execute([$uid]); $hash = $stmt->fetchColumn(); if (!$hash || !password_verify($d['old_password'], $hash)) return [403, ['error' => 'wrong_password']]; return db_try('users/password', function() use ($pdo, $d, $uid) { $stmt = $pdo->prepare("UPDATE users SET password = ?, force_password_change = 0 WHERE id = ?"); $stmt->execute([password_hash($d['new_password'], PASSWORD_DEFAULT), $uid]); return $stmt->rowCount() === 0 ? [404, ['error' => 'user_not_found']] : [200, ['msg' => 'ok']]; }); } /** Save or update a location detail. Admin only (checked in router). */ function api_details_save(PDO $pdo, array $d): array { if (trim($d['title'] ?? '') === '') return [400, ['error' => 'title is required']]; return db_try('details', fn() => ( $pdo->prepare("INSERT INTO task_details (title, address, description, related_person) VALUES (?, ?, ?, ?) ON CONFLICT(title) DO UPDATE SET address=excluded.address, description=excluded.description, related_person=excluded.related_person") ->execute([trim($d['title']), $d['address'], $d['description'], $d['related_person']]) ) ? [200, ['msg' => 'ok']] : [500, ['error' => 'Database error']]); } /** Delete a location detail by title. Admin only (checked in router). */ function api_details_delete(PDO $pdo, array $d): array { return db_try('details/delete', function() use ($pdo, $d) { $stmt = $pdo->prepare("DELETE FROM task_details WHERE title = ?"); $stmt->execute([$d['title']]); return $stmt->rowCount() === 0 ? [404, ['error' => 'not_found']] : [200, ['msg' => 'ok']]; }); }