{"id":50976428,"url":"https://github.com/codepress/ac-examples-bookings","last_synced_at":"2026-06-19T08:32:42.848Z","repository":{"id":365481970,"uuid":"1267247474","full_name":"codepress/ac-examples-bookings","owner":"codepress","description":"Admin Columns Pro demo for Bookings","archived":false,"fork":false,"pushed_at":"2026-06-17T15:35:43.000Z","size":458,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T16:29:17.301Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codepress.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-12T11:08:50.000Z","updated_at":"2026-06-17T15:39:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/codepress/ac-examples-bookings","commit_stats":null,"previous_names":["codepress/ac-examples-bookings"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/codepress/ac-examples-bookings","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codepress%2Fac-examples-bookings","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codepress%2Fac-examples-bookings/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codepress%2Fac-examples-bookings/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codepress%2Fac-examples-bookings/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codepress","download_url":"https://codeload.github.com/codepress/ac-examples-bookings/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codepress%2Fac-examples-bookings/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34523982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T08:32:42.173Z","updated_at":"2026-06-19T08:32:42.841Z","avatar_url":"https://github.com/codepress.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hotel Bookings — Admin Columns Pro Custom List Table example\n\nA small, self‑contained WordPress plugin that turns three plain database tables\ninto a fully‑featured admin screen using **Admin Columns Pro**'s *Custom List\nTables* feature (powered by the **Data Sources** addon).\n\nIt exists to be read. If you have your own custom tables — bookings, orders,\nevents, IoT readings, anything that lives outside `wp_posts` — this repo shows\nyou the smallest realistic amount of code needed to give them a sortable,\nfilterable, inline‑editable admin table, with related lookups resolved to\nhuman‑readable labels.\n\n![The Hotel Bookings admin screen generated by this example](screenshot.png)\n\n| | |\n|---|---|\n| **Requires** | Admin Columns Pro **7.1+** with the **Data Sources** addon active, PHP 7.4+ |\n| **Demo dataset** | ~210 bookings, 80 guests, 10 rooms |\n| **What you write** | One ~140‑line PHP class. No PHP view templates, no React, no `WP_List_Table` subclass. |\n\n\u003e Official guide: [How to set up Custom List Tables](https://docs.admincolumns.com/article/120-how-to-setup-custom-list-tables)\n\u003e More recipes: [Custom List Tables Cookbook](https://github.com/codepress/ac-custom-list-tables-cookbook)\n\n---\n\n## Get Started\n\nSeeing the result makes the code easier to read.\n\n1. Make sure **Admin Columns Pro 7.1+** is active with the **Data Sources**\n   addon enabled. (If it isn't, the plugin shows an admin notice explaining\n   why the table won't appear — see [`Requirements.php`](classes/Requirements.php).)\n2. [Download](https://github.com/codepress/ac-examples-bookings/archive/refs/heads/main.zip) the Hotel\n   Bookings example plugin and drop this folder into `wp-content/plugins/`. No\n   build step and no dependencies — the classes are loaded with plain `require`s\n   in the bootstrap, so there's nothing to install.\n3. Activate **\"ACP Sample Data – Hotel Bookings\"** in WordPress. Activation\n   automatically creates `wp_hbk_guests`, `wp_hbk_rooms` and `wp_hbk_bookings`\n   and loads the demo rows — no manual import step.\n4. Open the new **Hotel Bookings** menu item in the admin sidebar and explore the\n   available views.\n\nNeed to reinstall or start over? **Tools → Hotel Bookings Sample Data** has\n**\"Create \u0026 populate sample tables\"** and **\"Drop tables (reset)\"** buttons.\nDeactivating the plugin drops the tables (they're recreated on reactivation), and\ndeleting the plugin removes them too (see [`uninstall.php`](uninstall.php)).\n\n---\n\n## The idea in one minute\n\nWordPress gives you list tables for posts, pages, users and comments out of the\nbox. Anything else — your own custom tables — normally means subclassing\n`WP_List_Table`, writing column callbacks, wiring up sorting, pagination,\nfilters and bulk actions by hand. It's a lot of boilerplate.\n\nAdmin Columns Pro's Data Sources addon flips this around. You **describe** a\ntable to the addon — its name, which columns to show, how those columns should\nbehave, and how it relates to other tables — and the addon builds the entire\nadmin screen for you, including the bits Admin Columns is already good at:\nsorting, smart filters, inline editing, conditional formatting, export and\nfooter metrics.\n\nSo the work splits cleanly into two halves:\n\n1. **Code (this repo):** register the table and *type* its columns. Done once,\n   in PHP, on a hook.\n2. **UI (no code):** open the generated screen in Admin Columns and arrange\n   columns, set display formats, add filters and formatting rules. Stored in\n   the `wp_admin_columns` table, not here.\n\nThis example covers both — the code in full, and the UI steps as a checklist so\nyou can reproduce the polished result.\n\n---\n\n## The data model\n\nThree ordinary tables — nothing Admin Columns‑specific about them. This is\ndeliberate: the point is that *your existing schema* needs no changes.\n\n```\nwp_hbk_bookings                        wp_hbk_guests\n┌───────────────────────────┐            ┌──────────────────────────┐\n│ id            (PK)        │      ┌────▶│ id              (PK)     │\n│ reference                 │      │     │ first_name               │\n│ guest_id  ────────────────┼──────┘     │ last_name                │\n│ room_id   ────────────────┼──────┐     │ full_name  (generated)   │ ◀── label\n│ check_in   (unix ts)      │      │     │ email                    │\n│ check_out  (unix ts)      │      │     │ phone / country          │\n│ nights / guests_count     │      │     └──────────────────────────┘\n│ total_amount / amount_paid│      │\n│ status         (0–3)      │      │     wp_hbk_rooms\n│ payment_status (0–2)      │      │     ┌──────────────────────────┐\n│ source                    │      └────▶│ id              (PK)     │\n│ notes                     │            │ room_code                │\n│ created_at / updated_at   │            │ room_type                │ ◀── label\n└───────────────────────────┘            │ capacity / rate / active │\n                                         └──────────────────────────┘\n```\n\nTwo things in the schema are worth calling out because the registration code\nrelies on them:\n\n- **`check_in` / `check_out` are stored as Unix timestamps** (`int`), not\n  `DATETIME`. The code types them with the PHP date format `'U'` so the addon\n  knows how to read them.\n- **`status` and `payment_status` are integer codes** (`0,1,2,3`). The code\n  maps each code to a label so the cell shows \"Confirmed\", not `1`.\n- **`guests.full_name` is a generated column.** It's used as the guest table's\n  display label, so a related Guest column reads \"Mia van Dijk\" instead of `7`.\n\nFull schema and rows: [`data/sample-data.sql`](data/sample-data.sql).\n\n---\n\n## The code that matters\n\nEverything interesting is in\n[`classes/CustomListTableInit.php`](classes/CustomListTableInit.php). Read that\nfile alongside this section — it's heavily commented and short.\n\n### Where registration happens\n\nThe addon fires a hook when it's ready to collect data sources. You register on\nit. That's the entire integration surface.\n\n```php\nclass CustomListTableInit\n{\n    public function __construct()\n    {\n        add_action('acp/data-sources/register', [$this, 'register']);\n    }\n\n    public function register(DataSourceRegistry $registry): void\n    {\n        // ...build DataSource objects and $registry-\u003eregister(...) them\n    }\n}\n```\n\nIf Admin Columns Pro or the Data Sources addon isn't active, the hook never\nfires and nothing happens — no fatal errors. That's why\n[`Requirements.php`](classes/Requirements.php) detects the capability by class\nexistence (`class_exists('ACA\\\\DataSources\\\\DataSourceRegistry')`) rather than\ncomparing version strings, which keeps it working on pre‑release builds like\n`7.1beta`.\n\n### Anatomy of a Data Source\n\nA `DataSource` is three (sometimes four) things:\n\n```php\n$bookings = new DataSource(\n    new DataSourceId('hbk_bookings'),        // 1. a stable, unique id\n    Facade\\Table::from('wp_hbk_bookings'),   // 2. the table (+ optional label column)\n    $bookings_columns,                       // 3. column configuration\n    new Facade\\Relations([ /* ... */ ])      // 4. (optional) relations to other sources\n);\n```\n\n1. **`DataSourceId`** — a slug that identifies this source. It also determines\n   the admin page URL: the addon registers the screen as\n   `acp-data-sources-{id}`, so `hbk_bookings` → `admin.php?page=acp-data-sources-hbk_bookings`.\n   (See how [`AdminPage.php`](classes/SampleData/AdminPage.php) builds the\n   \"View the table →\" link from exactly this rule.)\n2. **`Facade\\Table::from($table, $label_column)`** — names the table. The\n   optional second argument is the *identifier/label column*: the column shown\n   when this source is referenced from elsewhere. The bookings table omits it\n   (defaults to the primary key); the lookups set it deliberately (below).\n3. **Column config** — which columns appear and how each behaves (next section).\n4. **Relations** — how this table joins to others (the section after that).\n\nYou register each source with `$registry-\u003eregister(new Entry($source))`. Only\nthe source you want a menu for gets one:\n\n```php\n$registry-\u003eregister(\n    Entry::create($bookings)\n        -\u003eset_menu('Hotel Bookings', 'Hotel Bookings', 'dashicons-calendar-alt', 25)\n);\n```\n\n`set_menu($page_title, $menu_title, $icon, $position)` is what makes **Hotel\nBookings** appear as a top‑level admin menu item. The two lookup tables are\nregistered *without* a menu — they exist only to feed the relations, so they\nshouldn't clutter the sidebar.\n\n### Typing columns\n\nBy default the addon shows columns as plain text. *Typing* a column tells the\naddon how to read and render the underlying value, which unlocks the right\ndefault display, sorting behaviour and inline‑edit control. This is the part\nworth getting right — good types mean almost no UI tweaking afterwards.\n\n```php\n$bookings_columns = Config\\Columns::create()\n    -\u003ewith_columns([\n        ColumnType\\TextType::for('reference')-\u003ewith_label('Ref.'),\n\n        // Stored as a Unix timestamp -\u003e pass the PHP date format 'U'.\n        ColumnType\\DateTimeType::for('check_in', 'U')-\u003ewith_label('Check-in'),\n        ColumnType\\DateTimeType::for('check_out', 'U')-\u003ewith_label('Check-out'),\n\n        ColumnType\\NumberType::for('total_amount')-\u003ewith_label('Total'),\n\n        // Integer codes -\u003e human labels.\n        ColumnType\\SelectColumnType::for('status', [\n            0 =\u003e 'Pending',\n            1 =\u003e 'Confirmed',\n            2 =\u003e 'Cancelled',\n            3 =\u003e 'Completed',\n        ])-\u003ewith_label('Status'),\n    ])\n    -\u003ewith_label_resolver(new HumanReadableResolver());\n```\n\nTypes used in this example:\n\n| Type | Use it for | Note |\n|---|---|---|\n| `TextType` | strings (`reference`, `source`) | |\n| `NumberType` | numeric columns (`nights`, `total_amount`, `rate`) | |\n| `DateTimeType::for($col, $format)` | dates/times | pass the PHP date format the column is stored in — here `'U'` for Unix timestamps |\n| `SelectColumnType::for($col, $map)` | integer/enum codes | the `$map` turns `1` into `Confirmed` |\n| `EmailType` | email addresses | gets a `mailto:` treatment |\n| `BooleanType` | yes/no flags (`active`) | |\n\n`with_label('…')` sets the column header. **`with_label_resolver(new\nHumanReadableResolver())`** is the catch‑all for every *untyped* column: it\nturns raw column names like `created_at` into \"Created At\" automatically, so you\nonly have to name the columns you care about.\n\nYou don't need to type every column — anything you skip still appears, just as\ngeneric text with a humanised header.\n\n### Relations: turning foreign keys into names\n\nA booking stores `guest_id = 7` and `room_id = 3`. On their own those are\nmeaningless integers. Relations tell the addon how to follow them.\n\nFirst, register the two lookup tables as their own data sources — each with a\n**label column** so the relation knows what to display:\n\n```php\n$guests = new DataSource(\n    new DataSourceId('hbk_guests'),\n    Facade\\Table::from('wp_hbk_guests', 'full_name'),   // \u003c- label column\n    Config\\Columns::create()\n        -\u003ewith_columns([ ColumnType\\EmailType::for('email')-\u003ewith_label('Email') ])\n        -\u003ewith_label_resolver(new HumanReadableResolver())\n);\n$registry-\u003eregister(new Entry($guests));                // no menu\n```\n\nThen declare the relations on the bookings source:\n\n```php\nnew Facade\\Relations([\n    // bookings.guest_id -\u003e guests.id, displayed as a \"Guest\" column\n    Facade\\Relation\\Column::has_one($guests, 'id', 'Guest', 'guest_id'),\n\n    // bookings.room_id  -\u003e rooms.id,   displayed as a \"Room\" column\n    Facade\\Relation\\Column::has_one($rooms, 'id', 'Room', 'room_id'),\n])\n```\n\n`has_one($target_source, $target_key, $label, $local_key)` reads as: *each\nbooking has one guest; match `bookings.guest_id` to `guests.id`; call the column\n\"Guest\".* Because the guests source declared `full_name` as its label column,\nthe Guest column renders \"Mia van Dijk\" — and because guests is itself a typed\ndata source, you can switch that column to show the email or any other guest\nfield from the Admin Columns UI, no code change needed.\n\nThis is the payoff of registering the lookups as real data sources rather than\nhard‑coding a join: every related table is itself fully typed and reusable.\n\n---\n\n## What you do in the UI (no code)\n\nThe code gives you a working, sensible table. The finishing touches live in\nAdmin Columns and are stored per‑view in `wp_admin_columns`. Open the **Hotel\nBookings** screen, click the Admin Columns settings, and:\n\n- **Columns** — choose which to show and their order.\n- **Guest column** → set its \"Column\" to **Guest** (`full_name`); **Room** →\n  **Room type**.\n- **Total / Paid** → display as **Currency**, EUR (`€1,234.00`).\n- **Check‑in / Check‑out** → date display format `j M Y` (e.g. `18 Jun 2026`).\n- **Status** → add **Conditional Formatting** colour pills (Pending amber,\n  Confirmed green, Cancelled red, Completed blue).\n- **Footer Metrics** → Total = *Sum* of Total, Avg booking = *Average* of Total,\n  Bookings = *Count* of Ref.\n- **Smart Filters** → enable on Status, Source, and a Check‑in date range.\n\nThe class docblock in\n[`CustomListTableInit.php`](classes/CustomListTableInit.php) lists these same\nsteps next to the code, so you can see which half does what.\n\n**You don't have to do any of this by hand.** This plugin ships two of these\narrangements as templates ([`data/*.json`](data/)) and **imports them as saved\nviews on first run** (see\n[`ImportTemplates.php`](classes/Service/ImportTemplates.php)), so the **Hotel\nBookings** screen opens with the finished layout — columns, formats, filters and\nformatting rules — already applied. Tweak from there, or use the Admin Columns\ntemplate picker to reload **\"Bookings Example\"** at any time. (The auto‑import\nruns once; deleting or editing the views won't make it run again.)\n\n---\n\n## How the plugin is wired together\n\nThe bootstrap is [`ac-examples-bookings.php`](ac-examples-bookings.php), and it\ndoes only a few things:\n\n```php\n(new Requirements())-\u003eregister();   // admin notice if ACP/Data Sources missing\n\nnew CustomListTableInit();          // \u003c-- the part you came here for\n\n(new AdminPage(                     // Tools page to install/reset the demo data\n    new Installer(__DIR__ . '/data/sample-data.sql')\n))-\u003eregister();\n\n(new LocalTemplates(                // ship the data/*.json column templates\n    new SplFileInfo(__DIR__ . '/data')\n))-\u003eregister();\n\n(new ImportTemplates(               // import those templates as saved views once\n    new SplFileInfo(__DIR__ . '/data')\n))-\u003eregister();\n```\n\n| File | Responsibility |\n|---|---|\n| [`ac-examples-bookings.php`](ac-examples-bookings.php) | Plugin header, `require`s the classes, bootstrap |\n| [`classes/CustomListTableInit.php`](classes/CustomListTableInit.php) | **The example.** Registers the data sources, types, relations and menu |\n| [`classes/Requirements.php`](classes/Requirements.php) | Detects whether ACP + Data Sources is active; shows a notice if not |\n| [`classes/PluginActionLinks.php`](classes/PluginActionLinks.php) | Adds an \"Edit Columns\" link to the plugin row, opening the column editor |\n| [`classes/SampleData/AdminPage.php`](classes/SampleData/AdminPage.php) | Tools → \"Hotel Bookings Sample Data\" page (install / reset) |\n| [`classes/SampleData/Installer.php`](classes/SampleData/Installer.php) | Creates/drops the demo tables, runs the bundled SQL dump |\n| [`classes/Service/LocalTemplates.php`](classes/Service/LocalTemplates.php) | Registers the bundled `data/*.json` column templates as pre‑defined templates |\n| [`classes/Service/ImportTemplates.php`](classes/Service/ImportTemplates.php) | Imports those templates as saved views once, on first run |\n| [`data/sample-data.sql`](data/sample-data.sql) | Schema + demo rows |\n| [`data/*.json`](data/) | Column templates — registered as loadable templates and auto‑imported as saved views |\n| [`uninstall.php`](uninstall.php) | Drops the demo tables when the plugin is deleted |\n\nThe sample‑data machinery (`AdminPage`, `Installer`) is *scaffolding for this\ndemo*, not part of the Custom List Tables pattern. You won't need it in your own\nplugin — your tables already exist. It's a good reference, though, for a clean\nadmin‑post form with nonce checks, the Post/Redirect/Get pattern, and a\ndefensive SQL‑dump runner that won't clobber populated tables.\n\n---\n\n## Adapting this to your own tables\n\nA practical checklist, mapping each step to where it lives in the example:\n\n1. **Pick the hook.** `add_action('acp/data-sources/register', …)` and accept\n   the `DataSourceRegistry`.\n2. **Register your main table** as a `DataSource` with a unique `DataSourceId`\n   and `Facade\\Table::from('your_table')`.\n3. **Type the columns** that need it (dates with their stored format, codes via\n   `SelectColumnType`, emails, numbers…) and add a `HumanReadableResolver` for\n   the rest.\n4. **Give it a menu** with `Entry::create($source)-\u003eset_menu(...)`.\n5. **For each foreign key**, register the referenced table as its own\n   (menu‑less) `DataSource` with a label column, then add a\n   `Facade\\Relation\\Column::has_one(...)` on the main source.\n6. **Open the screen** and finish the presentation in the Admin Columns UI.\n\nThen guard your plugin the way [`Requirements.php`](classes/Requirements.php)\ndoes, so it degrades gracefully when the addon isn't present.\n\nFor variations on this pattern — different relation types, more column types,\nedge cases — see the [Custom List Tables\nCookbook](https://github.com/codepress/ac-custom-list-tables-cookbook) and the\n[official documentation](https://docs.admincolumns.com/article/120-how-to-setup-custom-list-tables).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodepress%2Fac-examples-bookings","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodepress%2Fac-examples-bookings","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodepress%2Fac-examples-bookings/lists"}