[options]'); out(''); out('Business commands:'); out(' business:list [--active=1|0|all] [--q=text] [--limit=50]'); out(' business:show --id=ID|--slug=SLUG'); out(' business:activate --id=ID|--slug=SLUG'); out(' business:deactivate --id=ID|--slug=SLUG'); out(' business:update --id=ID|--slug=SLUG [field options]'); out(' business:set-video --id=ID|--slug=SLUG --video-url=URL'); out(' business:clear-video --id=ID|--slug=SLUG'); out(' business:delete --id=ID|--slug=SLUG --yes'); out(''); out('Business update field options:'); out(' --name=TEXT --slug=TEXT --category-id=ID --subcategory=TEXT'); out(' --description=TEXT --address=TEXT --phone=TEXT --email=TEXT'); out(' --website=TEXT --video-url=URL --hours-json=JSON'); out(' --lat=NUM --lng=NUM --active=1|0 --featured=1|0'); out(''); out('Menu commands:'); out(' menu:wipe-all --yes'); out(' menu:wipe-business --id=ID|--slug=SLUG --yes'); out(' menu:show --id=ID|--slug=SLUG'); out(''); out('Other commands:'); out(' categories:list'); out(''); out('Examples:'); out(' php scripts/keyser-admin.php business:deactivate --slug=royal-restaurant'); out(' php scripts/keyser-admin.php business:update --slug=queens-point-coffee --phone="304-555-0100" --featured=1'); out(' php scripts/keyser-admin.php business:set-video --slug=queens-point-coffee --video-url="https://youtu.be/example"'); out(' php scripts/keyser-admin.php business:clear-video --slug=queens-point-coffee'); out(' php scripts/keyser-admin.php menu:wipe-business --slug=castiglia-italian --yes'); exit(0); } function parseArgs(array $argv): array { $command = $argv[1] ?? 'help'; $options = []; for ($i = 2; $i < count($argv); $i++) { $arg = $argv[$i]; if (!str_starts_with($arg, '--')) { fail("Unexpected argument: $arg"); } $arg = substr($arg, 2); if ($arg === '') { fail('Empty option name.'); } if (str_contains($arg, '=')) { [$key, $value] = explode('=', $arg, 2); } else { $key = $arg; $value = '1'; } $options[str_replace('-', '_', $key)] = $value; } return [$command, $options]; } function opt(array $options, string $key, mixed $default = null): mixed { return $options[$key] ?? $default; } function requireYes(array $options): void { if (opt($options, 'yes') !== '1') { fail('This is destructive. Re-run with --yes if you are sure.'); } } function boolish(mixed $value, string $label): int { $value = strtolower(trim((string)$value)); return match ($value) { '1', 'yes', 'true', 'on', 'active', 'enabled' => 1, '0', 'no', 'false', 'off', 'inactive', 'disabled' => 0, default => fail("$label must be 1 or 0."), }; } function clipText(string $value, int $width): string { if (function_exists('mb_strimwidth')) { return mb_strimwidth($value, 0, $width, ''); } return strlen($value) > $width ? substr($value, 0, $width) : $value; } function businessFromOptions(array $options): array { $id = (int)opt($options, 'id', 0); $slug = trim((string)opt($options, 'slug', '')); if ($id > 0 && $slug !== '') { fail('Use either --id or --slug, not both.'); } if ($id <= 0 && $slug === '') { fail('Provide --id=ID or --slug=SLUG.'); } $business = $id > 0 ? qone("SELECT * FROM businesses WHERE id=?", [$id]) : qone("SELECT * FROM businesses WHERE slug=?", [$slug]); if (!$business) { fail('Business not found.'); } return $business; } function printBusiness(array $business): void { $category = qone("SELECT name, icon FROM categories WHERE id=?", [(int)$business['category_id']]); out('ID: '.$business['id']); out('Name: '.$business['name']); out('Slug: '.$business['slug']); out('Category: '.(($category['icon'] ?? '').' '.($category['name'] ?? 'Uncategorized'))); out('Subcategory: '.($business['subcategory'] ?: '-')); out('Address: '.($business['address'] ?: '-')); out('Phone: '.($business['phone'] ?: '-')); out('Email: '.($business['email'] ?: '-')); out('Website: '.($business['website'] ?: '-')); out('Video URL: '.(($business['video_url'] ?? '') ?: '-')); out('Active: '.((int)$business['is_active'] === 1 ? 'yes' : 'no')); out('Featured: '.((int)$business['is_featured'] === 1 ? 'yes' : 'no')); out('Lat/Lng: '.$business['lat'].', '.$business['lng']); out('Hours JSON: '.$business['hours']); out('Description: '.$business['description']); } function validateEmail(string $email): void { if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { fail('Invalid email address.'); } } function validateHoursJson(string $json): void { $decoded = json_decode($json, true); if (!is_array($decoded)) { fail('--hours-json must be a JSON object.'); } foreach (array_keys($decoded) as $key) { if (!is_string($key)) { fail('--hours-json must be a JSON object with day names as keys.'); } } } function setBusinessStatus(array $options, int $active): void { $business = businessFromOptions($options); qrun("UPDATE businesses SET is_active=? WHERE id=?", [$active, (int)$business['id']]); out(($active ? 'Activated' : 'Deactivated').' business: '.$business['name'].' (#'.$business['id'].')'); } function wipeBusinessMenu(int $businessId): array { $sectionIds = array_column(qall("SELECT id FROM menu_sections WHERE business_id=?", [$businessId]), 'id'); $itemCount = 0; if ($sectionIds) { $placeholders = implode(',', array_fill(0, count($sectionIds), '?')); $itemCount = (int)qval("SELECT COUNT(*) FROM menu_items WHERE section_id IN ($placeholders)", $sectionIds); } $sectionCount = count($sectionIds); qrun("DELETE FROM menu_sections WHERE business_id=?", [$businessId]); return [$sectionCount, $itemCount]; } [$command, $options] = parseArgs($argv); if (in_array($command, ['help', '--help', '-h'], true)) { usage(); } try { match ($command) { 'business:list' => (function () use ($options): void { $where = []; $params = []; $active = strtolower((string)opt($options, 'active', 'all')); if ($active !== 'all') { $where[] = 'b.is_active=?'; $params[] = boolish($active, '--active'); } $q = trim((string)opt($options, 'q', '')); if ($q !== '') { $where[] = '(b.name LIKE ? OR b.slug LIKE ? OR b.address LIKE ? OR b.subcategory LIKE ?)'; $like = "%$q%"; array_push($params, $like, $like, $like, $like); } $limit = max(1, min(500, (int)opt($options, 'limit', 50))); $sqlWhere = $where ? 'WHERE '.implode(' AND ', $where) : ''; $rows = qall(" SELECT b.id,b.name,b.slug,b.is_active,b.is_featured,COALESCE(c.name,'Uncategorized') cat FROM businesses b LEFT JOIN categories c ON c.id=b.category_id $sqlWhere ORDER BY b.name LIMIT $limit ", $params); foreach ($rows as $row) { out(sprintf( '#%-4d %-38s %-26s active=%d featured=%d slug=%s', $row['id'], clipText((string)$row['name'], 38), clipText((string)$row['cat'], 26), $row['is_active'], $row['is_featured'], $row['slug'] )); } out(count($rows).' business'.(count($rows) === 1 ? '' : 'es').' shown.'); })(), 'business:show' => printBusiness(businessFromOptions($options)), 'business:activate' => setBusinessStatus($options, 1), 'business:deactivate' => setBusinessStatus($options, 0), 'business:set-video' => (function () use ($options): void { $business = businessFromOptions($options); $url = trim((string)opt($options, 'video_url', '')); if ($url === '') { fail('Provide --video-url=URL. Use --video-url="" with business:update to clear it.'); } if (!validBusinessVideoUrl($url)) { fail('Video URL must be a YouTube or Vimeo URL.'); } qrun("UPDATE businesses SET video_url=? WHERE id=?", [$url, (int)$business['id']]); out('Updated video URL for '.$business['name'].'.'); })(), 'business:clear-video' => (function () use ($options): void { $business = businessFromOptions($options); qrun("UPDATE businesses SET video_url='' WHERE id=?", [(int)$business['id']]); out('Cleared video URL for '.$business['name'].'.'); })(), 'business:delete' => (function () use ($options): void { requireYes($options); $business = businessFromOptions($options); qrun("DELETE FROM businesses WHERE id=?", [(int)$business['id']]); out('Deleted business and related records: '.$business['name'].' (#'.$business['id'].')'); })(), 'business:update' => (function () use ($options): void { $business = businessFromOptions($options); $map = [ 'name' => 'name', 'slug' => 'slug', 'category_id' => 'category_id', 'subcategory' => 'subcategory', 'description' => 'description', 'address' => 'address', 'phone' => 'phone', 'email' => 'email', 'website' => 'website', 'video_url' => 'video_url', 'hours_json' => 'hours', 'lat' => 'lat', 'lng' => 'lng', 'active' => 'is_active', 'featured' => 'is_featured', ]; $set = []; $params = []; foreach ($map as $optionKey => $column) { if (!array_key_exists($optionKey, $options)) continue; $value = (string)$options[$optionKey]; if ($optionKey === 'email') validateEmail($value); if ($optionKey === 'video_url' && !validBusinessVideoUrl($value)) fail('Video URL must be a YouTube or Vimeo URL.'); if ($optionKey === 'hours_json') validateHoursJson($value); if ($optionKey === 'category_id') { $value = (string)(int)$value; if (!qval("SELECT id FROM categories WHERE id=?", [(int)$value])) fail('Category ID not found.'); } if (in_array($optionKey, ['active', 'featured'], true)) $value = (string)boolish($value, "--$optionKey"); if (in_array($optionKey, ['lat', 'lng'], true) && !is_numeric($value)) fail("--$optionKey must be numeric."); if ($optionKey === 'slug' && $value !== '') { $value = makeSlug($value); $clash = qval("SELECT id FROM businesses WHERE slug=? AND id!=?", [$value, (int)$business['id']]); if ($clash) fail('That slug is already used by another business.'); } $set[] = "$column=?"; $params[] = $value; } if (!$set) { fail('No update fields provided. Run help to see supported field options.'); } $params[] = (int)$business['id']; qrun("UPDATE businesses SET ".implode(',', $set)." WHERE id=?", $params); out('Updated business: '.$business['name'].' (#'.$business['id'].')'); })(), 'menu:show' => (function () use ($options): void { $business = businessFromOptions($options); $sections = qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order", [(int)$business['id']]); out($business['name'].' menu'); if (!$sections) { out('No menu sections found.'); return; } foreach ($sections as $section) { out(''); out('['.$section['id'].'] '.$section['name'].($section['description'] ? ' - '.$section['description'] : '')); $items = qall("SELECT * FROM menu_items WHERE section_id=? ORDER BY sort_order", [(int)$section['id']]); foreach ($items as $item) { out(sprintf( ' - [%d] %s%s%s', $item['id'], $item['name'], (float)$item['price'] > 0 ? ' $'.number_format((float)$item['price'], 2) : '', (int)$item['is_available'] === 1 ? '' : ' (unavailable)' )); } } })(), 'menu:wipe-business' => (function () use ($options): void { requireYes($options); $business = businessFromOptions($options); [$sections, $items] = wipeBusinessMenu((int)$business['id']); out("Deleted $sections menu section".($sections === 1 ? '' : 's')." and $items item".($items === 1 ? '' : 's')." for ".$business['name'].'.'); })(), 'menu:wipe-all' => (function () use ($options): void { requireYes($options); $sectionCount = (int)qval("SELECT COUNT(*) FROM menu_sections"); $itemCount = (int)qval("SELECT COUNT(*) FROM menu_items"); qrun("DELETE FROM menu_sections"); out("Deleted $sectionCount menu section".($sectionCount === 1 ? '' : 's')." and $itemCount item".($itemCount === 1 ? '' : 's').' across all businesses.'); })(), 'categories:list' => (function (): void { $rows = qall("SELECT id,name,icon,sort_order FROM categories ORDER BY sort_order,name"); foreach ($rows as $row) { out(sprintf('#%-3d %-2s %-32s sort=%d', $row['id'], $row['icon'], $row['name'], $row['sort_order'])); } })(), default => fail("Unknown command: $command. Run `php scripts/keyser-admin.php help`.", 2), }; } catch (PDOException $e) { fail($e->getMessage()); }