//<ๅญๅŠจไฝœ>` ๅŽ่ฐƒ่ฟ™้‡Œใ€‚ * * โš ๏ธ ไธบไป€ไนˆไธๅšๆˆ"ๆฏ่ต„ๆบไธ€ไธชๆ–‡ไปถ"๏ผšๆœฌ้กน็›ฎ็š„้ƒจ็ฝฒไธŽ้™ๆ€ๆ–ญ่จ€้ƒฝๆ˜ฏ**ๆŒ‰ๆ–‡ไปถ็™ฝๅๅ•**่ตฐ็š„ * ๏ผˆ่ง `familybook_deploy.sh`๏ผ‰๏ผŒๅ‡ ๅไธชๅฐๆ–‡ไปถไผš่ฎฉ"ๅ“ชไบ›่ฏฅ่ฟ›ๅŒ…"ๅ˜ๆˆไธ€ไปถ่ฆ็ปดๆŠค็š„ไบ‹ใ€‚ * ไธ€ไธชๆ–‡ไปถ + ๆธ…ๆ™ฐๅˆ†ๆฎต๏ผŒๆ˜ฏ่ฟ™้‡Œๆ›ด็จณ็š„้€‰ๆ‹ฉใ€‚ * โš ๏ธ ๆฏไธชๅ†™ๆ“ไฝœ**้ƒฝ**ๅ…ˆ่ฐƒ `Api::requireWrite()` โ€”โ€” ๅช่ฏป่ดฆๅทๅœจ็ฝ‘้กตไธŠไธ่ƒฝๅ†™๏ผŒ * ๅœจ API ้‡ŒไนŸๅฟ…้กปไธ่ƒฝๅ†™๏ผˆๆƒ้™่ฃ‚็ผๆ˜ฏๆœ€ไธ่ฏฅๅ‡บ็Žฐ็š„้‚ฃ็ง bug๏ผ‰ใ€‚ */ declare(strict_types=1); namespace App; final class ApiHandler { /** * @param string $res ่ต„ๆบๅ๏ผˆpeople / movies / โ€ฆ๏ผ‰ * @param int $id ่ทฏๅพ„้‡Œ็š„ๆ•ฐๅญ— id๏ผˆๆฒกๆœ‰ๅˆ™ 0๏ผ‰ * @param string $sub ๅญๅŠจไฝœ๏ผˆๅฆ‚ tasks/12/status ้‡Œ็š„ status๏ผ‰ * @param string $method GET / POST / PATCH / DELETE */ public function route(string $res, int $id, string $sub, string $method): void { // ๅฅๅบทๆฃ€ๆŸฅ / ็‰ˆๆœฌๆŽขๆต‹๏ผš**ไธ้œ€่ฆไปค็‰Œ**๏ผˆApp ๅฏๅŠจๆ—ถๅ…ˆๆŽข่ฟ™ไธช๏ผ‰ if ($res === 'ping' || $res === '') { Api::ok([ 'service' => 'familybook', 'api' => 'v1', 'time' => date('c'), ]); } switch ($res) { case 'me': $this->me($method); return; case 'tokens': $this->tokens($id, $method); return; case 'home': $this->home(); return; case 'people': $this->people($id, $sub, $method); return; case 'notifications': $this->notifications($id, $sub, $method); return; case 'calendar': $this->calendar($sub, $method); return; case 'events': $this->events($id, $sub, $method); return; case 'days': $this->days($id, $method); return; // ็บชๅฟตๆ—ฅ case 'tasks': $this->tasks($id, $sub, $method); return; case 'movies': $this->movies($id, $sub, $method); return; case 'cinemas': $this->cinemas($id, $method); return; case 'trips': $this->trips($id, $sub, $method); return; case 'ledger': $this->ledger($id, $sub, $method); return; case 'resources': $this->resources($id, $method); return; case 'books': $this->books($id, $sub, $method); return; case 'notes': $this->notes($id, $method); return; } Api::fail('ๆœช็Ÿฅ็š„ๆŽฅๅฃ่ต„ๆบ๏ผš' . $res, 'not_found', 404); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๆˆ‘ / ไปค็‰Œ / ้ฆ–้กต โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function me(string $method): void { [$fam, $pid] = Api::me(); $p = Person::find($pid) ?: []; Api::ok([ 'id' => $pid, 'name' => (string)($p['name'] ?? ''), 'avatar' => avatar_url($p['avatar'] ?? null), 'role' => Auth::roleWord(), 'family' => (function () use ($fam): array { $f = Family::find($fam) ?: []; return ['id' => $fam, 'name' => (string)($f['name'] ?? '')]; })(), // ๅ„ๆจกๅ—ไธป้ข˜่‰ฒ๏ผšApp ็ซฏ้กถ้ƒจ/ๅˆ†็ฑป่‰ฒๅ—็›ดๆŽฅ็”จๅฎƒ๏ผŒไธๅฟ…่‡ชๅทฑ็ปดๆŠคไธ€ๅผ ่กจ 'colors' => $this->moduleColors($fam), ]); } private function moduleColors(int $fam): array { $out = []; foreach (['calendar', 'tasks', 'movies', 'trips', 'ledger', 'resources', 'books', 'notes', 'people'] as $k) { $out[$k] = Module::color($k, $fam); } return $out; } /** ไปค็‰Œ่‡ชๅŠฉ็ฎก็†๏ผˆApp ้‡Œ"้€€ๅ‡บ็™ปๅฝ•/ๅŠ้”€่ฎพๅค‡"๏ผ‰ */ private function tokens(int $id, string $method): void { [, $pid] = Api::me(); if ($method === 'GET') { Api::ok(ApiToken::listFor($pid)); } if ($method === 'DELETE') { Api::requireWrite(); if ($id > 0) { Api::ok(['revoked' => ApiToken::revoke($pid, $id) ? 1 : 0]); } Api::ok(['revoked' => ApiToken::revokeAll($pid)]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } private function home(): void { [$fam, $pid] = Api::me(); $today = date('Y-m-d'); $cals = Calendar::mine($fam, $pid); $soon = Calendar::month($fam, $cals, $pid, $today, date('Y-m-d', strtotime('+7 days'))); $soon = array_values(array_filter($soon, static fn(array $e): bool => (string)($e['kind'] ?? '') !== 'lunar')); $todayEvents = array_values(array_filter($soon, static fn(array $e): bool => (string)($e['date'] ?? '') === $today)); $tasks = Task::byStatus($fam, $pid, 'doing'); foreach (Task::byStatus($fam, $pid, 'todo') as $t) { if (Task::isOverdue($t['due_at'] ?? null)) { $tasks[] = $t; } } Api::ok([ 'greeting' => Home::greeting( Nickname::display($pid, null) ?: '', $today, count($soon), count(array_filter($tasks, static fn($t) => Task::isOverdue($t['due_at'] ?? null)))), 'date' => $today, 'weekday' => Home::weekday($today), 'today_events' => $todayEvents, 'tasks' => $tasks, 'unread' => Notification::unreadCount($pid), ]); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๅๅ†Œ / ไบบ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function people(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $p = Person::find($id); // ๅช่ฎคๆœฌๅฎถ็š„๏ผˆ่ทจๅฎถ id ็›ดๆŽฅๅฝ“ไธๅญ˜ๅœจ๏ผŒไธๆณ„้œฒ"ๅˆซไบบๅฎถๆœ‰ๆฒกๆœ‰่ฟ™ไธชไบบ"๏ผ‰ if (!$p || (int)$p['family_id'] !== $fam) { Api::notFound('่ฟ™ไธชไบบ'); } if ($method === 'GET') { Api::ok($p); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Person::update($id, [ 'name' => Api::str('name', (string)$p['name'], 50), 'gender' => Api::str('gender', (string)($p['gender'] ?? '')), 'birthday'=> Api::dt('birthday', $p['birthday'] ?? null), 'note' => Api::str('note', (string)($p['note'] ?? '')), 'phone' => Api::str('phone', (string)($p['phone'] ?? ''), 40), ]); Api::ok(Person::find($id)); } if ($method === 'DELETE') { Api::requireWrite(); Person::delete($id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $name = Api::str('name', '', 50); if ($name === '') { Api::fail('ๅพ—ๅ†™ไธชๅๅญ—ใ€‚', 'unprocessable', 422); } $newId = Person::create($fam, [ 'name' => $name, 'kind' => Api::str('kind', 'member'), 'gender' => Api::str('gender', ''), 'note' => Api::str('note', ''), 'phone' => Api::str('phone', '', 40), ]); Api::ok(Person::find($newId), 201); } // ๅˆ—่กจ๏ผšๆ”ฏๆŒ ?q= ๆœ็ดขใ€?kind=member|contact $list = Person::all($fam, Api::str('q'), Api::str('kind', 'member'), $pid); Api::list(array_values($list)); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ้€š็Ÿฅ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function notifications(int $id, string $sub, string $method): void { [, $pid] = Api::me(); if ($sub === 'read' || ($method === 'POST' && $sub === 'read')) { Api::ok(['marked' => Notification::markAllRead($pid)]); } if ($sub === 'clear' || $method === 'DELETE') { if ($id > 0) { Api::ok(['deleted' => Notification::delete($id, $pid)]); } Api::ok(['cleared' => Notification::clearMine($pid)]); } Api::ok([ 'unread' => Notification::unreadCount($pid), 'items' => Notification::mine($pid, 50), ]); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๆ—ฅ็จ‹ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function calendar(string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($sub === 'books') { // GET /calendar/books Api::ok(Calendar::mine($fam, $pid)); } // GET /calendar?from=2026-10-01&to=2026-10-31 ๏ผˆ้ป˜่ฎคๆœฌๆœˆ๏ผ‰ $from = Api::str('from', date('Y-m-01')); $to = Api::str('to', date('Y-m-t')); $cals = Calendar::mine($fam, $pid); $rows = Calendar::month($fam, $cals, $pid, $from, $to); Api::ok($rows); } private function events(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $ev = $this->eventFor($fam, $id); if (!$ev) { Api::notFound('่ฟ™ๆกๆ—ฅ็จ‹'); } if (!Calendar::canWriteEvent($ev, $pid)) { Api::fail('่ฟ™ๆœฌๆ—ฅๅކไฝ ๆฒกๆœ‰ๅ†™ๆƒ้™ใ€‚', 'forbidden', 403); } if ($method === 'GET') { Api::ok($ev); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); $this->writeEvent($pid, $ev); } if ($method === 'DELETE') { Api::requireWrite(); $this->deleteEvent($ev); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $calId = Api::int('calendar_id'); $cal = Calendar::mineOne($fam, $pid, $calId); if (!$cal || !Calendar::canWrite($cal, $pid) || Calendar::isReadonlyKind((string)$cal['kind'])) { Api::fail('่ฟ™ๆœฌๆ—ฅๅކไธ่ƒฝๅ†™ๅ…ฅใ€‚', 'forbidden', 403); } $d = $this->eventData(null); if ($d['starts_at'] === '') { Api::fail('ๅผ€ๅง‹ๆ—ถ้—ดไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } if ($d['title'] === '') { Api::fail('ๆ—ฅ็จ‹ๅพ—ๆœ‰ไธชๆ ‡้ข˜ใ€‚', 'unprocessable', 422); } $d['created_by'] = $pid; $newId = Calendar::addEvent((int)$cal['id'], $d); Api::ok($this->eventFor($fam, $newId), 201); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } /** ๅ–ไธ€ๆกๆ—ฅ็จ‹๏ผˆๆœฌๅฎถ่Œƒๅ›ดๅ†…๏ผ›ไธๅญ˜ๅœจ่ฟ”ๅ›ž null๏ผ‰ */ private function eventFor(int $fam, int $id): ?array { $ev = Calendar::event($id); if (!$ev) { return null; } $cal = Calendar::find((int)$ev['calendar_id']); if (!$cal || (int)$cal['family_id'] !== $fam) { return null; } $ev['skips'] = Calendar::skipsOf($id); return $ev; } /** * ๆ—ฅ็จ‹ๅ†™ๅ…ฅๅญ—ๆฎต๏ผˆๆ–ฐๅปบไธŽๆ›ดๆ–ฐๅ…ฑ็”จ๏ผ›`$cur` ้ž็ฉบๅš"ๅชๆ”น็ป™ไบ†็š„ๅญ—ๆฎต"ๅ›ž้€€๏ผ‰ใ€‚ * * ๐Ÿ”ด ้‡ๅคๆ—ฅ็จ‹็š„**ไธ‰ๆกฃ่Œƒๅ›ด**๏ผˆๆ”น/ๅˆ ่ฟ™ไธ€ๆฌกใ€่ฟ™ไธ€ๆฌกๅŠไปฅๅŽใ€ๅ…จ้ƒจ๏ผ‰โ€”โ€” * ็ฝ‘้กต้‚ฃไธคๆฎต้€ป่พ‘๏ผˆevent_update / event_del๏ผ‰ๆฏ”่พƒ้•ฟ๏ผŒ่ฟ™้‡Œ**ไธๅคๅˆถ**ๅฎƒไปฌ๏ผŒ * ่€Œๆ˜ฏ่ฎฉ App ็”จๅŒไธ€็ป„ `scope` + `occ` ๅ‚ๆ•ฐ๏ผŒๅนถๅœจ API ้‡ŒๅšๅŒๆ ท็š„ไธ‰ๆกฃๅค„็†ใ€‚ * ๏ผˆๅคๅˆถไธคไปฝไธšๅŠก่ง„ๅˆ™ๆ˜ฏ"ๅฟ…็„ถ้•ฟๆญช"็š„ๅ†™ๆณ•๏ผ›่ฟ™้‡ŒๆŠŠๅทฎๅผ‚ๆ”ถๅœจไธ€ไธช็งๆœ‰ๆ–นๆณ•้‡Œใ€‚๏ผ‰ */ private function eventData(?array $cur): array { $has = static fn(string $k): bool => array_key_exists($k, Api::body()) || array_key_exists($k, $_POST) || array_key_exists($k, $_GET); $starts = $has('starts_at') ? Api::dt('starts_at') : (string)($cur['starts_at'] ?? ''); $ends = $has('ends_at') ? Api::dt('ends_at') : ($cur['ends_at'] ?? null); $allDay = $has('all_day') ? Api::bool('all_day') : !empty($cur['all_day']); if ($allDay) { $starts = $starts !== null && $starts !== '' ? substr((string)$starts, 0, 10) . ' 00:00:00' : (string)$starts; $ends = $ends !== null && $ends !== '' ? substr((string)$ends, 0, 10) . ' 00:00:00' : null; } return [ 'title' => $has('title') ? Api::str('title', '', 200) : (string)($cur['title'] ?? ''), 'location' => $has('location') ? Api::str('location', '', 255) : (string)($cur['location'] ?? ''), 'starts_at' => (string)($starts ?? ''), 'ends_at' => $ends, 'all_day' => $allDay, 'reminder_minutes' => $has('reminder_minutes') ? Api::param('reminder_minutes', []) : ($cur['reminder_minutes'] ?? ''), 'status' => $has('status') ? Api::str('status', 'confirmed') : (string)($cur['status'] ?? 'confirmed'), 'transp' => $has('transp') ? Api::bool('transp') : !empty($cur['transp']), 'rrule' => $has('freq') || $has('rrule') ? $this->rruleFromBody() : (string)($cur['rrule'] ?? ''), 'note' => $has('note') ? Api::str('note', '') : (string)($cur['note'] ?? ''), ]; } /** ๆ›ดๆ–ฐไธ€ๆกๆ—ฅ็จ‹๏ผˆๅซ้‡ๅคๆ—ฅ็จ‹็š„ไธ‰ๆกฃ่Œƒๅ›ด๏ผ‰ */ private function writeEvent(int $pid, array $ev): void { $d = $this->eventData($ev); $scope = Api::str('scope', 'all'); $occ = substr(Api::str('occ'), 0, 10); $rrule = (string)($ev['rrule'] ?? ''); $hasOcc = (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $occ); if ($rrule !== '' && $hasOcc && $scope === 'once') { // ๅชๆ”น่ฟ™ไธ€ๆฌก๏ผšๅŽŸ็ณปๅˆ—่ทณๆމ้‚ฃๅคฉ + ่ฝไธ€ๆก็‹ฌ็ซ‹ๅ•ๆฌก Calendar::detachOnce(['id' => (int)$ev['id'], 'calendar_id' => (int)$ev['calendar_id']], $occ, $d, $pid); Api::ok(['updated' => (int)$ev['id'], 'scope' => 'once']); } if ($rrule !== '' && $hasOcc && $scope === 'future') { // ่ฟ™ไธ€ๆฌกๅŠไปฅๅŽ๏ผš่€็ณปๅˆ—ๆˆชๅˆฐๅ‰ไธ€ๅคฉ๏ผŒๆ–ฐ็ณปๅˆ—ไปŽ่ฟ™ๅคฉ่ตท $rp = Repeat::parse($rrule); $cut = date('Y-m-d', strtotime($occ . ' -1 day')); $rp['until'] = $cut; $rp['count'] = 0; Calendar::updateEvent((int)$ev['id'], [ 'title' => (string)($ev['title'] ?? ''), 'location' => (string)($ev['location'] ?? ''), 'starts_at' => (string)$ev['starts_at'], 'ends_at' => $ev['ends_at'] ?: null, 'all_day' => (int)($ev['all_day'] ?? 0) === 1, 'reminder_minutes' => $ev['reminder_minutes'] ?? '', 'status' => (string)($ev['status'] ?? 'confirmed'), 'transp' => (int)($ev['transp'] ?? 0) === 1, 'rrule' => 'FREQ=' . strtoupper((string)$rp['freq']) . ($rp['freq'] === 'weekday' ? ';BYDAY=MO,TU,WE,TH,FR' : '') . ';UNTIL=' . str_replace('-', '', $cut) . 'T235959Z', 'note' => (string)($ev['note'] ?? ''), ]); $d2 = $d; $d2['starts_at'] = $occ . ' ' . substr((string)$d['starts_at'], 11, 8); $d2['ends_at'] = $d['ends_at'] !== null ? $occ . ' ' . substr((string)$d['ends_at'], 11, 8) : null; $d2['created_by'] = $pid; $newId = Calendar::addEvent((int)$ev['calendar_id'], $d2); Api::ok(['updated' => (int)$ev['id'], 'new_series' => $newId, 'scope' => 'future']); } Calendar::updateEvent((int)$ev['id'], $d); Api::ok($this->eventFor((int)Api::me()[0], (int)$ev['id'])); } /** ๅˆ ้™คไธ€ๆกๆ—ฅ็จ‹๏ผˆๅซ้‡ๅคๆ—ฅ็จ‹็š„ไธ‰ๆกฃ่Œƒๅ›ด๏ผ‰ */ private function deleteEvent(array $ev): void { $id = (int)$ev['id']; $scope = Api::str('scope', 'all'); $occ = substr(Api::str('occ'), 0, 10); $rrule = (string)($ev['rrule'] ?? ''); $hasOcc = (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $occ); if ($rrule !== '' && $hasOcc && $scope === 'once') { Calendar::skipOnce($id, $occ); Api::ok(['skipped' => $occ, 'scope' => 'once']); } if ($rrule !== '' && $hasOcc && $scope === 'future') { $rp = Repeat::parse($rrule); $cut = date('Y-m-d', strtotime($occ . ' -1 day')); $rp['count'] = 0; $rp['until'] = $cut; $firstDay = substr((string)$ev['starts_at'], 0, 10); if ($cut < $firstDay) { Calendar::deleteEvent($id); Api::ok(['deleted' => $id, 'scope' => 'future']); } $newRule = 'FREQ=' . strtoupper((string)$rp['freq']) . (!empty($rp['interval']) && (int)$rp['interval'] > 1 ? ';INTERVAL=' . (int)$rp['interval'] : '') . ($rp['freq'] === 'weekday' ? ';BYDAY=MO,TU,WE,TH,FR' : '') . ';UNTIL=' . str_replace('-', '', $cut) . 'T235959Z'; Database::get()->prepare('UPDATE calendar_events SET rrule = ?, sequence = sequence + 1 WHERE id = ?') ->execute([$newRule, $id]); Api::ok(['truncated_to' => $cut, 'scope' => 'future']); } Calendar::deleteEvent($id); Api::ok(['deleted' => $id]); } /** * ้‡ๅค่ง„ๅˆ™๏ผšๆŽฅๅ—ไธค็งๅ†™ๆณ• โ€”โ€” * ยท `rrule` ็›ดๆŽฅ็ป™ RFC 5545 ๅŽŸๆ–‡๏ผˆๅฆ‚ `FREQ=WEEKLY;COUNT=5`๏ผ‰๏ผ› * ยท ๆˆ–็”จ `freq` + `rep_end` + `rep_count` / `rep_until` ไธ‰ไธชๅญ—ๆฎต๏ผˆไธŽ็ฝ‘้กต่กจๅ•ๅŒๆž„๏ผ‰ใ€‚ * โš ๏ธ ๅŽ่€…ๆ˜ฏๅ…ณ้”ฎ๏ผšApp ็ซฏๅš้‡ๅค่ฎพ็ฝฎๆ—ถๆœ€่‡ช็„ถ็š„ๅฐฑๆ˜ฏ"้€‰้ข‘็އใ€้€‰็ป“ๆŸๆ–นๅผ"๏ผŒ * ไธ่ฏฅ้€ผๅฎƒๅŽปๆ‹ผ RRULE ๅญ—็ฌฆไธฒ๏ผˆ้‚ฃๆ˜ฏๆŠŠๆœๅŠก็ซฏ็š„ๅฎž็Žฐ็ป†่Š‚ๆผ็ป™ไบ†ๅฎขๆˆท็ซฏ๏ผ‰ใ€‚ */ private function rruleFromBody(): string { $raw = Api::str('rrule'); if ($raw !== '') { return $raw; } $freq = Api::str('freq'); if ($freq === '') { return ''; } return Repeat::build([ 'freq' => $freq, 'rep_end' => Api::str('rep_end', 'never'), 'rep_count' => Api::int('rep_count', 10), 'rep_until' => Api::str('rep_until', ''), ]); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ็บชๅฟตๆ—ฅ๏ผˆdays๏ผ‰ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function days(int $id, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $a = Anniversary::find($fam, $id); if (!$a) { Api::notFound('่ฟ™ๆก็บชๅฟตๆ—ฅ'); } if ($method === 'GET') { Api::ok($a); } if ($method === 'DELETE') { Api::requireWrite(); Anniversary::remove($fam, $id); Api::ok(['deleted' => $id]); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Anniversary::save($fam, $this->annivData($a), $id); Api::ok(Anniversary::find($fam, $id)); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $d = $this->annivData(null); if ($d['title'] === '') { Api::fail('็บชๅฟตๆ—ฅๅพ—ๆœ‰ไธชๅๅญ—ใ€‚', 'unprocessable', 422); } $newId = Anniversary::save($fam, $d, 0); Api::ok(Anniversary::find($fam, $newId), 201); } Api::ok(Anniversary::all($fam, $pid)); } /** ็บชๅฟตๆ—ฅๅญ—ๆฎต๏ผˆๅฅ‘็บฆไธŽ days/save ่ทฏ็”ฑไธ€่‡ด๏ผ‰ */ private function annivData(?array $cur): array { $has = static fn(string $k): bool => array_key_exists($k, Api::body()) || array_key_exists($k, $_POST) || array_key_exists($k, $_GET); return [ 'title' => $has('title') ? Api::str('title', '', 100) : (string)($cur['title'] ?? ''), 'repeat_type' => $has('repeat_type') ? Api::str('repeat_type', 'solar') : (string)($cur['repeat_type'] ?? 'solar'), 'month' => $has('month') ? Api::int('month', 1) : (int)($cur['month'] ?? 1), 'day' => $has('day') ? Api::int('day', 1) : (int)($cur['day'] ?? 1), 'is_leap' => $has('is_leap') ? Api::bool('is_leap') : !empty($cur['is_leap']), 'kind' => $has('kind') ? Api::str('kind', '', 40) : (string)($cur['kind'] ?? ''), 'note' => $has('note') ? Api::str('note', '') : (string)($cur['note'] ?? ''), 'person_ids' => $has('person_ids') ? Api::ints('person_ids') : array_map(static fn($p) => (int)$p['id'], (array)($cur['people'] ?? [])), ]; } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ไปปๅŠก โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function tasks(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $t = Task::find($fam, $id, $pid); if (!$t) { Api::notFound('่ฟ™้กนไปปๅŠก'); } // ๅญๅŠจไฝœ๏ผš็Šถๆ€ / ๆฌ็œ‹ๆฟ / ่ฏ„่ฎบ if ($sub === 'status' && ($method === 'POST' || $method === 'PATCH')) { Api::requireWrite(); Task::setStatus($fam, $id, Api::str('status', 'todo'), Api::int('position')); Api::ok(Task::find($fam, $id, $pid)); } if ($sub === 'move' && $method === 'POST') { Api::requireWrite(); Task::moveToBoard($fam, $id, Api::int('board_id'), Api::int('before'), $pid); Api::ok(Task::find($fam, $id, $pid)); } if ($sub === 'comments') { if ($method === 'GET') { Api::ok(Task::comments($id)); } if ($method === 'POST') { Api::requireWrite(); $txt = Api::str('content', '', 4000); if ($txt === '') { Api::fail('่ฏ„่ฎบไธ่ƒฝๆ˜ฏ็ฉบ็š„ใ€‚', 'unprocessable', 422); } Api::ok(['id' => Task::addComment($id, $pid, $txt)], 201); } } if ($method === 'GET') { Api::ok($t); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Task::update($fam, $id, [ 'title' => Api::str('title', (string)$t['title'], 200), 'note' => Api::str('note', (string)($t['note'] ?? '')), 'due_at' => Api::dt('due_at', $t['due_at'] ?? null), 'repeat_rule' => Api::str('repeat_rule', (string)($t['repeat_rule'] ?? '')), 'parent_id' => Api::int('parent_id', (int)($t['parent_id'] ?? 0)), 'assignee_id' => Api::int('assignee_id', (int)($t['assignee_id'] ?? 0)), 'category_id' => Api::int('category_id', (int)($t['category_id'] ?? 0)), 'board_id' => Api::int('board_id', (int)($t['board_id'] ?? 0)), 'status' => (string)$t['status'], // ็Šถๆ€ๅช่ตฐ /status ๅญๅŠจไฝœ ]); Api::ok(Task::find($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); Task::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $title = Api::str('title', '', 200); if ($title === '') { Api::fail('ไปปๅŠกๅพ—ๅ†™ๆธ…ๆฅš่ฆๅšไป€ไนˆใ€‚', 'unprocessable', 422); } $newId = Task::create($fam, $pid, [ 'title' => $title, 'note' => Api::str('note', ''), 'due_at' => Api::dt('due_at', ''), 'repeat_rule' => Api::str('repeat_rule', ''), 'parent_id' => Api::int('parent_id'), // ๐Ÿ”ด v44 ็ฌฌ 5 ๆกใ€Œ่ดŸ่ดฃไบบ้ป˜่ฎคๆ˜ฏ่‡ชๅทฑใ€โ€”โ€”API ๅปบไปปๅŠกๆ—ถๅŒๆ ท้ป˜่ฎค่‡ชๅทฑ๏ผŒ // ไธŽ็ฝ‘้กต่กจๅ•ไธ€่‡ด๏ผˆไธไผ  assignee_id ๅฐฑๆ˜ฏ"ๆˆ‘่ดŸ่ดฃ"๏ผ‰ใ€‚ 'assignee_id' => Api::int('assignee_id', $pid), 'category_id' => Api::int('category_id'), 'board_id' => Api::int('board_id'), 'status' => Api::str('status', 'todo'), ]); foreach (Api::people(Api::ints('shares'), $fam) as $sp) { Task::setShare($newId, $sp, 'write'); } Api::ok(Task::find($fam, $newId, $pid), 201); } // ---- ๅˆ—่กจ๏ผšไธ‰็ง่ง†ๅ›พ ---- // ?view=board โ†’ ๆŒ‰็œ‹ๆฟๅˆ†็ป„๏ผˆ้ป˜่ฎค๏ผ‰ // ?view=status&status=doing โ†’ ่ทจ็œ‹ๆฟๆŒ‰็Šถๆ€ // ?view=done โ†’ ๅทฒๅฎŒๆˆ๏ผˆๅซๅˆ†็ฑป/ๅ…ณ้”ฎ่ฏ่ฟ‡ๆปค๏ผ‰ $view = Api::str('view', 'board'); if ($view === 'status') { Api::ok(Task::byStatus($fam, $pid, Api::str('status', 'todo'))); } if ($view === 'done') { Api::ok(Task::doneList($fam, $pid, Api::int('category_id'), Api::str('keyword'))); } // ็œ‹ๆฟ่ง†ๅ›พ๏ผšๆŠŠ"ๅˆ—"ไธŽ"ไปปๅŠก"ไธ€่ตท็ป™ๅ‡บๆฅ๏ผˆApp ไธ€ๆฌก่ฏทๆฑ‚ๅฐฑๅคŸ็”ปๆ•ดๅฑ๏ผ‰ $boards = Task::boards($fam); /* โš ๏ธ `Task::boardColumns()` ่ฟ”ๅ›ž็š„ๆ˜ฏ**ๅŒ…ไบ†ไธ€ๅฑ‚็š„ๆ•ฐ็ป„** ๏ผˆ`['cols'=>[boardId => rows], 'boards'=>โ€ฆ, 'defId'=>โ€ฆ]`๏ผ‰๏ผŒ ไธๆ˜ฏ"boardId => rows"ๆœฌ่บซ โ€”โ€” ็›ดๆŽฅๆŒ‰ boardId ๅ–ไผšๆฐธ่ฟœๆ‹ฟๅˆฐ็ฉบๅˆ— ๏ผˆๅฎžๆต‹๏ผšๆŽฅๅฃ่ฟ”ๅ›ž็š„ boards ้‡Œ tasks ๅ…จๆ˜ฏ []๏ผŒ่€Œๆ–ญ่จ€ๅช็œ‹"้”ฎๅœจไธๅœจ"ๅฐฑๆผ่ฟ‡ๅŽปไบ†๏ผ‰ใ€‚ โ‡’ ๅฟ…้กปๅ– `['cols']` ้‚ฃไธ€ๅฑ‚ใ€‚ */ $bc = Task::boardColumns($fam, $pid); $cols = $bc['cols'] ?? []; $out = []; foreach ($boards as $b) { $out[] = [ 'id' => (int)$b['id'], 'name' => (string)$b['name'], 'color' => Task::boardColor($b['color'] ?? null, (string)$b['name']), 'tasks' => array_values($cols[(int)$b['id']] ?? []), ]; } if (!empty($cols[0])) { // ่ฟ˜ๆฒกๅฝ’ๅˆฐไปปไฝ•็œ‹ๆฟ็š„ๅ…œๅบ•ๅˆ— $out[] = ['id' => 0, 'name' => 'ๅ…ถไป–', 'color' => '', 'tasks' => array_values($cols[0])]; } Api::ok([ 'boards' => $out, 'categories' => Task::categories($fam), 'status' => Task::STATUS, 'repeat' => Task::REPEAT, ]); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ่ง‚ๅฝฑ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function movies(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $m = Movie::find($fam, $id, $pid); if (!$m) { Api::notFound('่ฟ™ๆก่ง‚ๅฝฑ่ฎฐๅฝ•'); } if ($sub === 'comments') { if ($method === 'GET') { $rv = MovieReview::mine($id, $pid); Api::ok($rv ? MovieReview::forEvent($id) : []); } if ($method === 'POST') { Api::requireWrite(); $rv = MovieReview::mine($id, $pid); if (!$rv) { Api::fail('ๅ…ˆๅ†™ไธ€ๆก่ฏ„ไปท๏ผŒๆ‰่ƒฝ่ฏ„่ฎบใ€‚', 'unprocessable', 422); } $txt = Api::str('content', '', 4000); if ($txt === '') { Api::fail('่ฏ„่ฎบไธ่ƒฝๆ˜ฏ็ฉบ็š„ใ€‚', 'unprocessable', 422); } $cid = MovieReview::addComment((int)$rv['id'], $pid, $txt, Api::int('parent_id'), Api::str('target_person', '')); if ($cid <= 0) { Api::fail('่ฏ„่ฎบๆฒกไฟๅญ˜ไธ‹ๆฅ๏ผˆๅฏ่ƒฝๅ†…ๅฎนๅคช้•ฟ๏ผ‰ใ€‚', 'unprocessable', 422); } Api::ok(['id' => $cid], 201); } } if ($sub === 'review' && ($method === 'POST' || $method === 'PUT' || $method === 'PATCH')) { Api::requireWrite(); $my = MovieReview::mine($id, $pid); $err = MovieReview::save( $id, $fam, $pid, Api::int('rating'), Api::str('content', '', 8000), array_values((array)Api::param('feelings', [])), $my !== null ); if ($err !== '') { Api::fail($err, 'unprocessable', 422); } Api::ok(MovieReview::mine($id, $pid)); } if ($method === 'GET') { Api::ok($m); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Movie::update($fam, $id, $this->movieData($m)); Api::ok(Movie::find($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); Movie::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $d = $this->movieData(null); if ($d['title'] === '') { Api::fail('็‰‡ๅไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } if ($d['starts_at'] === '') { Api::fail('่ง‚ๅฝฑๆ—ถ้—ดไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } $newId = Movie::create($fam, $pid, $d); Api::ok(Movie::find($fam, $newId, $pid), 201); } $list = Movie::all($fam, Api::str('keyword'), $pid); [$per, $off] = Api::page(50); Api::list(array_slice(array_values($list), $off, $per), count($list)); } /** ่ง‚ๅฝฑๅ†™ๅ…ฅๅญ—ๆฎต๏ผˆๆ–ฐๅปบไธŽๆ›ดๆ–ฐๅ…ฑ็”จ๏ผ›`$cur` ้ž็ฉบๆ—ถๅš"ๅชๆ”น็ป™ไบ†็š„ๅญ—ๆฎต"็š„ๅ›ž้€€๏ผ‰ */ private function movieData(?array $cur): array { $b = Api::body(); $pick = static function (string $k, $def) use ($b) { return array_key_exists($k, $b) || array_key_exists($k, $_POST) || array_key_exists($k, $_GET) ? null : $def; // null = "ๆœ‰ไบบ็ป™ไบ†"๏ผŒไธ‹้ขๆŒ‰ Api::* ๅ–็œŸๅฎžๅ€ผ }; $has = static fn(string $k): bool => array_key_exists($k, Api::body()) || array_key_exists($k, $_POST) || array_key_exists($k, $_GET); return [ 'title' => $has('title') ? Api::str('title', '', 200) : (string)($cur['title'] ?? ''), 'starts_at' => $has('starts_at') ? (string)(Api::dt('starts_at') ?? '') : (string)($cur['starts_at'] ?? ''), 'ends_at' => $has('ends_at') ? (string)(Api::dt('ends_at') ?? '') : (string)($cur['ends_at'] ?? ''), 'location' => $has('location') ? Api::str('location', '', 255) : (string)($cur['location'] ?? ''), 'note' => $has('note') ? Api::str('note', '') : (string)($cur['note'] ?? ''), 'info' => $has('info') ? Api::str('info', '') : (string)($cur['info'] ?? ''), 'cinema_id' => $has('cinema_id') ? Api::int('cinema_id') : (int)($cur['cinema_id'] ?? 0), 'attendees' => $has('attendees') ? Api::ints('attendees') : array_map( static fn($a) => (int)$a['id'], (array)($cur['attendees'] ?? [])), ]; } private function cinemas(int $id, string $method): void { [$fam] = Api::me(); if ($method === 'GET') { Api::ok(Cinema::all($fam)); } if ($method === 'POST' || $method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); $cur = $id > 0 ? Cinema::find($fam, $id) : null; if ($id > 0 && !$cur) { Api::notFound('่ฟ™ๅฎถๅฝฑ้™ข'); } $name = Api::str('name', (string)($cur['name'] ?? ''), 100); if ($name === '') { Api::fail('ๅฝฑ้™ขๅ็งฐไธ่ƒฝไธบ็ฉบ', 'unprocessable', 422); } $err = Cinema::save( $fam, $id, Api::str('place', (string)($cur['place'] ?? ''), 100), $name, Api::str('address', (string)($cur['address'] ?? ''), 200) ?: null, Cinema::normalizeColor(Api::str('color', (string)($cur['color'] ?? '#888888'), 16)) ); if ($err !== '') { Api::fail($err, 'conflict', 409); } // save() ไธ่ฟ”ๅ›ž id โ‡’ ๆ–ฐๅปบๆ—ถๆŒ‰"ๅŒๅœฐๆ–นๅŒๅ"ๅ›žๆŸฅ๏ผˆ่กจไธŠๆœ‰ๅ”ฏไธ€ๆ€งๆ ก้ชŒ๏ผŒๅคŸ็ฒพ็กฎ๏ผ‰ if ($id === 0) { $id = 0; foreach (Cinema::all($fam) as $c) { if ((string)$c['name'] === $name && (string)($c['place'] ?? '') === Api::str('place')) { $id = (int)$c['id']; break; } } } Api::ok(Cinema::find($fam, $id), $cur ? 200 : 201); } if ($method === 'DELETE' && $id > 0) { Api::requireWrite(); Cinema::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๆ—…็จ‹ / ่กŒ็จ‹ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function trips(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $t = Trip::find($fam, $id, $pid); if (!$t) { Api::notFound('่ฟ™่ถŸๆ—…็จ‹'); } if ($sub === 'nodes') { if ($method === 'GET') { Api::ok(Trip::nodes($id, $pid)); } if ($method === 'POST') { Api::requireWrite(); $nid = Trip::saveNode($fam, $id, $this->nodeData(null)); Api::ok(Trip::node($nid), 201); } } if ($sub === 'summary' && $method === 'GET') { Api::ok(Trip::summary($id, $pid)); } if ($method === 'GET') { Api::ok($t); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); $d = [ 'title' => Api::str('title', (string)$t['title'], 200), 'note' => Api::str('note', (string)($t['note'] ?? '')), 'summary' => Api::str('summary', (string)($t['summary'] ?? '')), ]; if (array_key_exists('is_public', Api::body())) { $d['is_public'] = Api::bool('is_public') ? '1' : '0'; } Trip::update($fam, $id, $d); Api::ok(Trip::find($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); Trip::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $title = Api::str('title', '', 200); if ($title === '') { Api::fail('ๆ—…็จ‹ๅพ—ๆœ‰ไธชๅๅญ—ใ€‚', 'unprocessable', 422); } $d = ['title' => $title, 'note' => Api::str('note', ''), 'summary' => Api::str('summary', '')]; if (array_key_exists('is_public', Api::body())) { $d['is_public'] = Api::bool('is_public') ? '1' : '0'; } $newId = Trip::create($fam, $pid, $d); Api::ok(Trip::find($fam, $newId, $pid), 201); } Api::ok(Trip::all($fam, Api::str('keyword'))); } /** ่กŒ็จ‹่Š‚็‚นๅญ—ๆฎต๏ผˆไธŽ็ฝ‘้กต node_add ๅŒไธ€ไปฝๅฅ‘็บฆ๏ผ‰ */ private function nodeData(?array $cur): array { $has = static fn(string $k): bool => array_key_exists($k, Api::body()) || array_key_exists($k, $_POST) || array_key_exists($k, $_GET); return [ 'title' => $has('title') ? Api::str('title', '', 200) : (string)($cur['title'] ?? ''), 'detail' => $has('detail') ? Api::str('detail', '') : (string)($cur['detail'] ?? ''), 'place' => $has('place') ? Api::str('place', '', 200) : (string)($cur['place'] ?? ''), 'starts_at' => $has('starts_at') ? (string)(Api::dt('starts_at') ?? '') : (string)($cur['starts_at'] ?? ''), 'ends_at' => $has('ends_at') ? (string)(Api::dt('ends_at') ?? '') : (string)($cur['ends_at'] ?? ''), 'node_type' => $has('node_type') ? Api::str('node_type', 'other') : (string)($cur['node_type'] ?? 'other'), 'status' => $has('status') ? Api::str('status', 'planned') : (string)($cur['status'] ?? 'planned'), 'category' => $has('category') ? Api::str('category', 'plan') : (string)($cur['category'] ?? 'plan'), // stage ไธๅœจ่ฟ™้‡Œๆ”น๏ผšๅช็”ฑๆ–ฐๅปบ/ๆ˜พๅผๅŠจไฝœๅ†ณๅฎš๏ผˆไธŽ็ฝ‘้กต node_add ๅŒไธ€ๆก็บชๅพ‹๏ผ‰ 'stage' => $cur === null ? Api::str('stage', 'plan') : (string)($cur['stage'] ?? 'plan'), 'members' => $has('members') ? Api::ints('members') : [], 'travel_mode' => Api::str('travel_mode', (string)($cur['travel_mode'] ?? '')), 'vehicle_id' => Api::int('vehicle_id', (int)($cur['vehicle_id'] ?? 0)), 'line_name' => Api::str('line_name', ''), 'carrier' => Api::str('carrier', ''), 'trip_no' => Api::str('trip_no', ''), ]; } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ่ฎฐ่ดฆ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function ledger(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $lg = Ledger::find($fam, $id, $pid); if (!$lg) { Api::notFound('่ฟ™ๆœฌ่ดฆๆœฌ'); } if ($sub === 'entries') { if ($method === 'GET') { $es = Ledger::entries($id); Api::ok(['items' => $es, 'summary' => Ledger::summary($es)]); } if ($method === 'POST') { Api::requireWrite(); // โš ๏ธ `amount` ไบค็ป™ๆจกๅž‹่‡ชๅทฑ่ฝฌๅˆ†๏ผˆLedger::toCents๏ผ‰โ€”โ€” App ไผ ๅ…ƒ๏ผŒ // ไธŽ็ฝ‘้กต่กจๅ•ๅŒไธ€ๅฃๅพ„๏ผ›่ฟ™้‡Œ**ไธ่ฆ**ๅ…ˆ่ฝฌไธ€ๆฌก๏ผˆไผšๅŒ้‡ๆข็ฎ—๏ผ‰ใ€‚ $amount = Api::param('amount', ''); if (trim((string)$amount) === '') { Api::fail('้‡‘้ขไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } $eid = Ledger::addEntry($id, [ 'kind' => Api::str('kind', 'expense'), 'amount' => $amount, 'title' => Api::str('title', '', 120), 'note' => Api::str('note', ''), 'person_id' => Api::int('person_id'), ], $pid); Api::ok(['id' => $eid], 201); } } if ($method === 'GET') { $es = Ledger::entries($id); Api::ok(['ledger' => $lg, 'summary' => Ledger::summary($es)]); } if ($method === 'DELETE') { Api::requireWrite(); Ledger::remove($fam, $id, $pid); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $name = Api::str('name', '', 80); /* โš ๏ธ `Ledger::defaultName()` **้œ€่ฆไธ€ไธช ownerId ๅ‚ๆ•ฐ**๏ผˆๅฎžๆต‹๏ผš ไธ็ป™ๅ‚ๆ•ฐไผš ArgumentCountError๏ผ‰โ€”โ€” ็”จๅฝ“ๅ‰ไบบ่กฅไธŠใ€‚ */ $newId = Ledger::create($fam, $pid, $name !== '' ? $name : Ledger::defaultName($pid)); Api::ok(Ledger::find($fam, $newId, $pid), 201); } Api::ok(Ledger::mine($fam, $pid)); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ่ต„ๆบ / ไนฆ / ็ฌ”่ฎฐ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ private function resources(int $id, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $r = Resource::find($fam, $id, $pid); if (!$r) { Api::notFound('่ฟ™ๆก่ต„ๆบ'); } if ($method === 'GET') { Api::ok($r); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Resource::update($fam, $id, [ 'title' => Api::str('title', (string)$r['title'], 200), 'content' => Api::str('content', (string)($r['content'] ?? '')), 'category'=> Api::str('category', (string)($r['category'] ?? '')), ]); Api::ok(Resource::find($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); Resource::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $title = Api::str('title', '', 200); if ($title === '') { Api::fail('่ต„ๆบๅพ—ๆœ‰ไธชๆ ‡้ข˜ใ€‚', 'unprocessable', 422); } $newId = Resource::create($fam, $pid, $pid, [ 'title' => $title, 'content' => Api::str('content', ''), 'category' => Api::str('category', ''), ]); Api::ok(Resource::find($fam, $newId, $pid), 201); } Api::ok(Resource::all($fam, $pid, Api::int('owner_id'), Api::str('keyword'), Api::str('sort', 'used'))); } private function books(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $bk = Book::find($fam, $id); if (!$bk) { Api::notFound('่ฟ™ๆœฌไนฆ'); } if ($sub === 'notes' && $method === 'GET') { Api::ok(Book::notes($id, $pid)); } if ($sub === 'notes' && $method === 'POST') { Api::requireWrite(); $txt = Api::str('content', '', 8000); if ($txt === '') { Api::fail('็ฌ”่ฎฐๅ†…ๅฎนไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } $nid = Book::addNote($fam, $id, $pid, [ 'content' => $txt, 'kind' => Api::str('kind', 'note'), 'chapter' => Api::str('chapter', '', 120), 'loc' => Api::str('loc', '', 120), ]); if ($nid <= 0) { Api::fail('็ฌ”่ฎฐๆฒกไฟๅญ˜ไธ‹ๆฅใ€‚', 'unprocessable', 422); } Api::ok(['id' => $nid], 201); } if ($method === 'GET') { Api::ok($bk); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); $d = $bk; if (array_key_exists('status', Api::body())) { $d['status'] = Api::str('status'); } if (array_key_exists('current_page', Api::body())) { $d['current_page'] = Api::str('current_page'); } if (array_key_exists('rating', Api::body())) { $d['rating'] = Api::int('rating'); } Book::update($fam, $id, $d); Api::ok(Book::find($fam, $id)); } if ($method === 'DELETE') { Api::requireWrite(); Book::destroy($fam, $id); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $title = Api::str('title', '', 200); if ($title === '') { Api::fail('ไนฆๅไธ่ƒฝไธบ็ฉบใ€‚', 'unprocessable', 422); } $newId = Book::create($fam, $pid, [ 'title' => $title, 'author' => Api::str('author', '', 120), 'category' => Api::str('category', ''), 'status' => Api::str('status', 'want'), ]); Api::ok(Book::find($fam, $newId), 201); } Api::ok(Book::all($fam, [ 'status' => Api::str('status'), 'category' => Api::str('category'), 'owner' => Api::int('owner_id'), 'keyword' => Api::str('keyword'), ])); } private function notes(int $id, string $method): void { [$fam, $pid] = Api::me(); if ($id > 0) { $n = Note::find($fam, $id, $pid); if (!$n) { Api::notFound('่ฟ™ๆก็ฌ”่ฎฐ'); } if ($method === 'GET') { Api::ok($n); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Note::update($fam, $id, $pid, [ 'title' => Api::str('title', (string)$n['title'], 200), 'body' => Api::str('body', (string)($n['body'] ?? '')), 'tags' => Api::param('tags', $n['tags'] ?? ''), ]); Api::ok(Note::find($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); Note::destroy($fam, $id, $pid); Api::ok(['deleted' => $id]); } Api::fail('ไธๆ”ฏๆŒ็š„ๆ–นๆณ•ใ€‚', 'method_not_allowed', 405); } if ($method === 'POST') { Api::requireWrite(); $title = Api::str('title', '', 200); if ($title === '') { Api::fail('็ฌ”่ฎฐๅพ—ๆœ‰ไธชๆ ‡้ข˜ใ€‚', 'unprocessable', 422); } $newId = Note::create($fam, $pid, [ 'title' => $title, 'body' => Api::str('body', ''), 'tags' => Api::param('tags', []), ]); Api::ok(Note::find($fam, $newId, $pid), 201); } [$per, $off] = Api::page(50); Api::ok([ 'tags' => Note::tagCloud($fam, $pid), 'items' => Note::all($fam, $pid, Api::str('keyword'), Api::str('tag'), $per, $off), ]); } }