@stacksjs/defaults 0.70.207 → 0.70.208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/TODO.md +21 -0
  2. package/ai/AGENTS.md +79 -0
  3. package/ai/README.md +45 -0
  4. package/ai/claude/launch.json +12 -0
  5. package/ai/skills/stacks-actions/SKILL.md +128 -0
  6. package/ai/skills/stacks-ai/SKILL.md +276 -0
  7. package/ai/skills/stacks-alias/SKILL.md +94 -0
  8. package/ai/skills/stacks-analytics/SKILL.md +91 -0
  9. package/ai/skills/stacks-api/SKILL.md +738 -0
  10. package/ai/skills/stacks-arrays/SKILL.md +277 -0
  11. package/ai/skills/stacks-auth/SKILL.md +422 -0
  12. package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
  13. package/ai/skills/stacks-brandkit/SKILL.md +811 -0
  14. package/ai/skills/stacks-browse/SKILL.md +127 -0
  15. package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
  16. package/ai/skills/stacks-browser/SKILL.md +145 -0
  17. package/ai/skills/stacks-buddy/SKILL.md +676 -0
  18. package/ai/skills/stacks-build/SKILL.md +163 -0
  19. package/ai/skills/stacks-cache/SKILL.md +125 -0
  20. package/ai/skills/stacks-calendar/SKILL.md +113 -0
  21. package/ai/skills/stacks-chat/SKILL.md +371 -0
  22. package/ai/skills/stacks-cli/SKILL.md +112 -0
  23. package/ai/skills/stacks-cloud/SKILL.md +192 -0
  24. package/ai/skills/stacks-cms/SKILL.md +217 -0
  25. package/ai/skills/stacks-collections/SKILL.md +82 -0
  26. package/ai/skills/stacks-commerce/SKILL.md +114 -0
  27. package/ai/skills/stacks-composables/SKILL.md +161 -0
  28. package/ai/skills/stacks-config/SKILL.md +144 -0
  29. package/ai/skills/stacks-configuration/SKILL.md +154 -0
  30. package/ai/skills/stacks-cron/SKILL.md +123 -0
  31. package/ai/skills/stacks-crosswind/SKILL.md +190 -0
  32. package/ai/skills/stacks-dashboard/SKILL.md +173 -0
  33. package/ai/skills/stacks-database/SKILL.md +286 -0
  34. package/ai/skills/stacks-datetime/SKILL.md +269 -0
  35. package/ai/skills/stacks-dependencies/SKILL.md +111 -0
  36. package/ai/skills/stacks-deploy/SKILL.md +102 -0
  37. package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
  38. package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
  39. package/ai/skills/stacks-design-output/SKILL.md +55 -0
  40. package/ai/skills/stacks-design-soft/SKILL.md +156 -0
  41. package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
  42. package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
  43. package/ai/skills/stacks-desktop/SKILL.md +78 -0
  44. package/ai/skills/stacks-development/SKILL.md +298 -0
  45. package/ai/skills/stacks-dns/SKILL.md +149 -0
  46. package/ai/skills/stacks-docs/SKILL.md +109 -0
  47. package/ai/skills/stacks-email/SKILL.md +236 -0
  48. package/ai/skills/stacks-enums/SKILL.md +100 -0
  49. package/ai/skills/stacks-env/SKILL.md +181 -0
  50. package/ai/skills/stacks-error-handling/SKILL.md +137 -0
  51. package/ai/skills/stacks-events/SKILL.md +228 -0
  52. package/ai/skills/stacks-faker/SKILL.md +158 -0
  53. package/ai/skills/stacks-git/SKILL.md +158 -0
  54. package/ai/skills/stacks-guard/SKILL.md +98 -0
  55. package/ai/skills/stacks-health/SKILL.md +75 -0
  56. package/ai/skills/stacks-http/SKILL.md +316 -0
  57. package/ai/skills/stacks-i18n/SKILL.md +198 -0
  58. package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
  59. package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
  60. package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
  61. package/ai/skills/stacks-investigate/SKILL.md +119 -0
  62. package/ai/skills/stacks-jobs/SKILL.md +136 -0
  63. package/ai/skills/stacks-lint/SKILL.md +184 -0
  64. package/ai/skills/stacks-listeners/SKILL.md +99 -0
  65. package/ai/skills/stacks-logging/SKILL.md +78 -0
  66. package/ai/skills/stacks-mail/SKILL.md +120 -0
  67. package/ai/skills/stacks-middleware/SKILL.md +249 -0
  68. package/ai/skills/stacks-migrations/SKILL.md +105 -0
  69. package/ai/skills/stacks-models/SKILL.md +267 -0
  70. package/ai/skills/stacks-new-feature/SKILL.md +206 -0
  71. package/ai/skills/stacks-notifications/SKILL.md +210 -0
  72. package/ai/skills/stacks-objects/SKILL.md +205 -0
  73. package/ai/skills/stacks-office-hours/SKILL.md +134 -0
  74. package/ai/skills/stacks-orm/SKILL.md +342 -0
  75. package/ai/skills/stacks-path/SKILL.md +118 -0
  76. package/ai/skills/stacks-payments/SKILL.md +373 -0
  77. package/ai/skills/stacks-plan-review/SKILL.md +152 -0
  78. package/ai/skills/stacks-plugins/SKILL.md +149 -0
  79. package/ai/skills/stacks-push/SKILL.md +376 -0
  80. package/ai/skills/stacks-query-builder/SKILL.md +128 -0
  81. package/ai/skills/stacks-queue/SKILL.md +559 -0
  82. package/ai/skills/stacks-realtime/SKILL.md +442 -0
  83. package/ai/skills/stacks-redesign/SKILL.md +182 -0
  84. package/ai/skills/stacks-registry/SKILL.md +58 -0
  85. package/ai/skills/stacks-repl/SKILL.md +80 -0
  86. package/ai/skills/stacks-retro/SKILL.md +122 -0
  87. package/ai/skills/stacks-review/SKILL.md +135 -0
  88. package/ai/skills/stacks-router/SKILL.md +231 -0
  89. package/ai/skills/stacks-routes/SKILL.md +105 -0
  90. package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
  91. package/ai/skills/stacks-scheduler/SKILL.md +363 -0
  92. package/ai/skills/stacks-search-engine/SKILL.md +112 -0
  93. package/ai/skills/stacks-security/SKILL.md +129 -0
  94. package/ai/skills/stacks-security-audit/SKILL.md +128 -0
  95. package/ai/skills/stacks-server/SKILL.md +458 -0
  96. package/ai/skills/stacks-shell/SKILL.md +62 -0
  97. package/ai/skills/stacks-slug/SKILL.md +87 -0
  98. package/ai/skills/stacks-sms/SKILL.md +385 -0
  99. package/ai/skills/stacks-socials/SKILL.md +303 -0
  100. package/ai/skills/stacks-storage/SKILL.md +576 -0
  101. package/ai/skills/stacks-strings/SKILL.md +416 -0
  102. package/ai/skills/stacks-stx/SKILL.md +176 -0
  103. package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
  104. package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
  105. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
  106. package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
  107. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
  108. package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
  109. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
  110. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
  111. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
  112. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
  113. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
  114. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
  115. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
  116. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
  117. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
  118. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
  119. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
  120. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
  121. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
  122. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
  123. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
  124. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
  125. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
  126. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
  127. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
  128. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
  129. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
  130. package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
  131. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
  132. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
  133. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
  134. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
  135. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
  136. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
  137. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
  138. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
  139. package/ai/skills/stacks-testing/SKILL.md +129 -0
  140. package/ai/skills/stacks-tunnel/SKILL.md +125 -0
  141. package/ai/skills/stacks-types/SKILL.md +178 -0
  142. package/ai/skills/stacks-ui/SKILL.md +246 -0
  143. package/ai/skills/stacks-utils/SKILL.md +444 -0
  144. package/ai/skills/stacks-validation/SKILL.md +371 -0
  145. package/ai/skills/stacks-whois/SKILL.md +315 -0
  146. package/assets/world-map-data.ts +121 -0
  147. package/bootstrap.ts +114 -0
  148. package/docs/api-examples.md +46 -0
  149. package/docs/index.md +24 -0
  150. package/docs/markdown-examples.md +87 -0
  151. package/functions/analytics.ts +97 -0
  152. package/functions/api.ts +143 -0
  153. package/functions/auth.ts +208 -0
  154. package/functions/billing/payments.ts +149 -0
  155. package/functions/blog.ts +120 -0
  156. package/functions/cms/categorizables.ts +107 -0
  157. package/functions/cms/pages.ts +108 -0
  158. package/functions/cms/posts.ts +100 -0
  159. package/functions/cms/taggables.ts +102 -0
  160. package/functions/commerce/coupons.ts +128 -0
  161. package/functions/commerce/customers.ts +128 -0
  162. package/functions/commerce/gift-cards.ts +119 -0
  163. package/functions/commerce/orders.ts +112 -0
  164. package/functions/commerce/payments.ts +119 -0
  165. package/functions/commerce/products/categories.ts +123 -0
  166. package/functions/commerce/products/manufacturers.ts +123 -0
  167. package/functions/commerce/products/products.ts +123 -0
  168. package/functions/commerce/products/reviews.ts +123 -0
  169. package/functions/commerce/products/units.ts +123 -0
  170. package/functions/commerce/products/variants.ts +123 -0
  171. package/functions/commerce/shippings/delivery-routes.ts +123 -0
  172. package/functions/commerce/shippings/digital-deliveries.ts +119 -0
  173. package/functions/commerce/shippings/drivers.ts +123 -0
  174. package/functions/commerce/shippings/license-keys.ts +123 -0
  175. package/functions/commerce/shippings/shipping-methods.ts +123 -0
  176. package/functions/commerce/shippings/shipping-rates.ts +119 -0
  177. package/functions/commerce/shippings/shipping-zones.ts +119 -0
  178. package/functions/commerce/taxes.ts +119 -0
  179. package/functions/commerce/waitlist/products.ts +123 -0
  180. package/functions/commerce/waitlist/restaurants.ts +123 -0
  181. package/functions/craft.ts +107 -0
  182. package/functions/dashboard.ts +153 -0
  183. package/functions/inbox.ts +95 -0
  184. package/functions/jobs.ts +86 -0
  185. package/functions/monitoring/errors.ts +295 -0
  186. package/functions/queue.ts +111 -0
  187. package/functions/realtime/websockets.ts +96 -0
  188. package/functions/realtime.ts +52 -0
  189. package/functions/releases.ts +49 -0
  190. package/functions/search.ts +126 -0
  191. package/functions/toasts.ts +95 -0
  192. package/functions/uploads.ts +216 -0
  193. package/ide/.gitignore +4 -0
  194. package/ide/cursor/rules/auto-imports.mdc +574 -0
  195. package/ide/cursor/rules/code-style.mdc +12 -0
  196. package/ide/cursor/rules/documentation.mdc +9 -0
  197. package/ide/cursor/rules/error-handling-validation.mdc +11 -0
  198. package/ide/cursor/rules/key-conventions.mdc +11 -0
  199. package/ide/cursor/rules/project-structure.mdc +169 -0
  200. package/ide/cursor/rules/readme.mdc +318 -0
  201. package/ide/cursor/rules/syntax-formatting.mdc +9 -0
  202. package/ide/cursor/rules/testing.mdc +10 -0
  203. package/ide/cursor/rules/typescript.mdc +9 -0
  204. package/ide/cursor/rules/ui-styling.mdc +0 -0
  205. package/ide/dictionary.txt +397 -0
  206. package/ide/jetbrains/.fleet/settings.json +4 -0
  207. package/ide/jetbrains/.idea/.gitignore +5 -0
  208. package/ide/jetbrains/.idea/.name +1 -0
  209. package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
  210. package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
  211. package/ide/jetbrains/.idea/modules.xml +8 -0
  212. package/ide/jetbrains/.idea/stacks.iml +22 -0
  213. package/ide/jetbrains/.idea/vcs.xml +6 -0
  214. package/ide/jetbrains/.idea/webResources.xml +14 -0
  215. package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
  216. package/ide/jetbrains/installed.txt +4 -0
  217. package/ide/jetbrains/options/github-copilot.xml +5 -0
  218. package/ide/jetbrains/options/nodejs.xml +7 -0
  219. package/ide/jetbrains/options/templates.xml +124 -0
  220. package/ide/jetbrains/templates/JavaScript.xml +6 -0
  221. package/ide/jetbrains/templates/Stacks.xml +8 -0
  222. package/ide/vscode/.vscode/README.md +23 -0
  223. package/ide/vscode/.vscode/extensions.json +5 -0
  224. package/ide/vscode/.vscode/settings.json +176 -0
  225. package/ide/vscode/.vscode/stacks.code-snippets +181 -0
  226. package/ide/vscode/LICENSE.md +21 -0
  227. package/ide/vscode/README.md +36 -0
  228. package/ide/vscode/logo.png +0 -0
  229. package/ide/vscode/package.json +79 -0
  230. package/ide/zed/.zed/settings.json +139 -0
  231. package/lang/README.md +28 -0
  232. package/lang/de.yml +14 -0
  233. package/lang/en.yml +15 -0
  234. package/package.json +19 -3
  235. package/project/bootstrap +161 -0
  236. package/project/buddy +175 -0
  237. package/project/storage/framework/tsconfig.app.json +50 -0
  238. package/project/storage/framework/tsconfig.base.json +58 -0
  239. package/routes/core.ts +78 -0
  240. package/routes/dashboard-api.ts +233 -0
  241. package/routes/dashboard.ts +739 -0
  242. package/routes/email.ts +129 -0
  243. package/stores/[name].stx +16 -0
  244. package/stores/git.ts +71 -0
  245. package/stores/payment.ts +474 -0
  246. package/stores/queue.ts +43 -0
  247. package/stores/user.ts +29 -0
  248. package/stx-components-plugin.ts +25 -0
  249. package/styles/design-tokens.css +597 -0
  250. package/types/dashboard.ts +70 -0
  251. package/types/defaults.ts +438 -0
  252. package/vcs/github/CODE_OF_CONDUCT.md +83 -0
  253. package/vcs/github/CONTRIBUTING.md +187 -0
  254. package/vcs/github/EXPLANATIONS.md +41 -0
  255. package/vcs/github/FUNDING.yml +1 -0
  256. package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  257. package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
  258. package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
  259. package/vcs/github/SECURITY.md +70 -0
  260. package/vcs/github/renovate.json +5 -0
  261. package/vcs/github/stale.yml +14 -0
  262. package/vcs/github/workflows/README.md +16 -0
  263. package/vcs/github/workflows/ci.yml +114 -0
  264. package/vcs/github/workflows/export-size.yml +25 -0
  265. package/vcs/github/workflows/labeler.yml +13 -0
  266. package/vcs/github/workflows/release.yml +219 -0
  267. package/views/[...all].stx +8 -0
  268. package/views/about.md +3 -0
  269. package/views/auth/login.stx +1 -0
  270. package/views/auth/signup.stx +85 -0
  271. package/views/dashboard/.discovered-models.json +542 -0
  272. package/views/dashboard/AUDIT.md +904 -0
  273. package/views/dashboard/[...all].stx +89 -0
  274. package/views/dashboard/access-tokens/index.stx +177 -0
  275. package/views/dashboard/actions/index.stx +129 -0
  276. package/views/dashboard/analytics/blog/index.stx +356 -0
  277. package/views/dashboard/analytics/browsers/index.stx +263 -0
  278. package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
  279. package/views/dashboard/analytics/commerce/web/index.stx +379 -0
  280. package/views/dashboard/analytics/countries/index.stx +289 -0
  281. package/views/dashboard/analytics/devices/index.stx +305 -0
  282. package/views/dashboard/analytics/events/index.stx +536 -0
  283. package/views/dashboard/analytics/index.stx +88 -0
  284. package/views/dashboard/analytics/marketing/index.stx +315 -0
  285. package/views/dashboard/analytics/pages/index.stx +271 -0
  286. package/views/dashboard/analytics/referrers/index.stx +319 -0
  287. package/views/dashboard/analytics/web/index.stx +356 -0
  288. package/views/dashboard/buddy/index.stx +198 -0
  289. package/views/dashboard/ci/index.stx +462 -0
  290. package/views/dashboard/cloud/index.stx +452 -0
  291. package/views/dashboard/commands/index.stx +295 -0
  292. package/views/dashboard/commerce/categories/index.stx +406 -0
  293. package/views/dashboard/commerce/coupons/index.stx +485 -0
  294. package/views/dashboard/commerce/customers/index.stx +410 -0
  295. package/views/dashboard/commerce/dashboard/index.stx +287 -0
  296. package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
  297. package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
  298. package/views/dashboard/commerce/delivery/drivers.stx +397 -0
  299. package/views/dashboard/commerce/delivery/index.stx +18 -0
  300. package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
  301. package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
  302. package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
  303. package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
  304. package/views/dashboard/commerce/delivery/tabs.ts +25 -0
  305. package/views/dashboard/commerce/gift-cards/index.stx +561 -0
  306. package/views/dashboard/commerce/manufacturers/index.stx +308 -0
  307. package/views/dashboard/commerce/orders/index.stx +247 -0
  308. package/views/dashboard/commerce/payments/index.stx +535 -0
  309. package/views/dashboard/commerce/pos/index.stx +653 -0
  310. package/views/dashboard/commerce/printers/devices/index.stx +273 -0
  311. package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
  312. package/views/dashboard/commerce/products/detail.stx +563 -0
  313. package/views/dashboard/commerce/products/index.stx +370 -0
  314. package/views/dashboard/commerce/reviews/index.stx +656 -0
  315. package/views/dashboard/commerce/reviews/index.ts +0 -0
  316. package/views/dashboard/commerce/taxes/index.stx +377 -0
  317. package/views/dashboard/commerce/units/index.stx +317 -0
  318. package/views/dashboard/commerce/variants/index.stx +319 -0
  319. package/views/dashboard/commerce/waitlist/products.stx +942 -0
  320. package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
  321. package/views/dashboard/components/index.stx +330 -0
  322. package/views/dashboard/composables/index.ts +19 -0
  323. package/views/dashboard/composables/useChart.ts +77 -0
  324. package/views/dashboard/composables/useNavigation.ts +99 -0
  325. package/views/dashboard/composables/useRole.ts +58 -0
  326. package/views/dashboard/composables/useTheme.ts +74 -0
  327. package/views/dashboard/content/authors/index.stx +171 -0
  328. package/views/dashboard/content/blog/index.stx +493 -0
  329. package/views/dashboard/content/categories/index.stx +169 -0
  330. package/views/dashboard/content/comments/index.stx +189 -0
  331. package/views/dashboard/content/dashboard.stx +383 -0
  332. package/views/dashboard/content/files/index.stx +706 -0
  333. package/views/dashboard/content/pages/index.stx +174 -0
  334. package/views/dashboard/content/posts/index.stx +269 -0
  335. package/views/dashboard/content/seo/index.stx +240 -0
  336. package/views/dashboard/content/tags/index.stx +167 -0
  337. package/views/dashboard/data/activity/index.stx +261 -0
  338. package/views/dashboard/data/dashboard/index.stx +116 -0
  339. package/views/dashboard/data/subscribers.stx +270 -0
  340. package/views/dashboard/data/teams.stx +275 -0
  341. package/views/dashboard/data/users.stx +279 -0
  342. package/views/dashboard/dependencies/index.stx +178 -0
  343. package/views/dashboard/deployments/[id].stx +234 -0
  344. package/views/dashboard/deployments/index.stx +9 -0
  345. package/views/dashboard/dns/index.stx +163 -0
  346. package/views/dashboard/environment/index.stx +283 -0
  347. package/views/dashboard/errors/index.stx +274 -0
  348. package/views/dashboard/forgot-password.stx +57 -0
  349. package/views/dashboard/functions/index.stx +159 -0
  350. package/views/dashboard/health/index.stx +248 -0
  351. package/views/dashboard/inbox/activity.stx +350 -0
  352. package/views/dashboard/inbox/index.stx +452 -0
  353. package/views/dashboard/inbox/settings.stx +290 -0
  354. package/views/dashboard/index.stx +252 -0
  355. package/views/dashboard/insights/index.stx +529 -0
  356. package/views/dashboard/jobs/[id].stx +430 -0
  357. package/views/dashboard/jobs/history.stx +222 -0
  358. package/views/dashboard/jobs/index.stx +214 -0
  359. package/views/dashboard/kanban/[id].stx +899 -0
  360. package/views/dashboard/kanban/index.stx +260 -0
  361. package/views/dashboard/layouts/default.stx +565 -0
  362. package/views/dashboard/layouts/guest.stx +10 -0
  363. package/views/dashboard/login.stx +48 -0
  364. package/views/dashboard/logs/index.stx +540 -0
  365. package/views/dashboard/mailboxes/index.stx +149 -0
  366. package/views/dashboard/management/permissions/index.stx +490 -0
  367. package/views/dashboard/marketing/campaigns/index.stx +604 -0
  368. package/views/dashboard/marketing/lists/index.stx +514 -0
  369. package/views/dashboard/marketing/reviews/index.stx +475 -0
  370. package/views/dashboard/marketing/social-posts/index.stx +560 -0
  371. package/views/dashboard/models/[model].stx +195 -0
  372. package/views/dashboard/models/index.stx +159 -0
  373. package/views/dashboard/monitoring/errors/index.stx +415 -0
  374. package/views/dashboard/notifications/dashboard.stx +383 -0
  375. package/views/dashboard/notifications/email.stx +336 -0
  376. package/views/dashboard/notifications/history.stx +373 -0
  377. package/views/dashboard/notifications/sms.stx +332 -0
  378. package/views/dashboard/packages/index.stx +195 -0
  379. package/views/dashboard/queries/[id].stx +381 -0
  380. package/views/dashboard/queries/history.stx +227 -0
  381. package/views/dashboard/queries/index.stx +335 -0
  382. package/views/dashboard/queries/slow.stx +318 -0
  383. package/views/dashboard/queue/index.stx +427 -0
  384. package/views/dashboard/realtime/index.stx +429 -0
  385. package/views/dashboard/register.stx +46 -0
  386. package/views/dashboard/releases/index.stx +389 -0
  387. package/views/dashboard/requests/index.stx +462 -0
  388. package/views/dashboard/serverless/index.stx +475 -0
  389. package/views/dashboard/servers/[id].stx +299 -0
  390. package/views/dashboard/servers/index.stx +483 -0
  391. package/views/dashboard/settings/billing.stx +50 -0
  392. package/views/dashboard/settings/index.stx +256 -0
  393. package/views/dashboard/settings/mail.stx +252 -0
  394. package/views/dashboard/stores/auth.ts +123 -0
  395. package/views/dashboard/stores/ci.ts +342 -0
  396. package/views/dashboard/stores/index.ts +129 -0
  397. package/views/dashboard/stores/kanban.ts +723 -0
  398. package/views/dashboard/stores/rbac.ts +350 -0
  399. package/views/dashboard/stx.config.ts +34 -0
  400. package/views/dashboard/teams/[id].stx +170 -0
  401. package/views/forum/category.stx +92 -0
  402. package/views/forum/index.stx +209 -0
  403. package/views/forum/new-topic.stx +113 -0
  404. package/views/forum/profile.stx +1 -0
  405. package/views/forum/topic.stx +143 -0
  406. package/views/hello-world/[name].stx +47 -0
  407. package/views/index.stx +21 -0
  408. package/views/system-tray/index.stx +147 -0
@@ -0,0 +1,1024 @@
1
+ @extends('layouts/default')
2
+
3
+ @section('content')
4
+ @title('Dashboard - Restaurant Waitlist')
5
+ <PageLayout>
6
+ <div class="sm:flex sm:items-center sm:justify-between">
7
+ <PageHeader title="Waitlist" description="Manage your restaurant waitlist and notify parties when their table is ready" />
8
+ <div class="flex mt-4 space-x-3 sm:mt-0">
9
+ <button type="button" @click="openAddEntryModal()" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 shadow-sm">
10
+ <div class="mr-1 h-5 w-5 i-hugeicons-plus-sign"></div>
11
+ Add to waitlist
12
+ </button>
13
+ <button type="button" @click="exportWaitlist()" class="inline-flex items-center px-3 py-2 font-semibold text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-800 hover:bg-gray-50 dark:hover:bg-neutral-700 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 shadow-sm">
14
+ <div class="mr-1 h-5 w-5 i-hugeicons-download-01"></div>
15
+ Export
16
+ </button>
17
+ </div>
18
+ </div>
19
+
20
+ <dl class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 mt-5">
21
+ <div class="overflow-hidden px-4 py-5 sm:p-6 bg-white dark:bg-neutral-800 rounded-lg shadow">
22
+ <dt class="font-medium text-gray-500 text-sm truncate dark:text-neutral-300">Current Wait</dt>
23
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ averageWait() }}min</dd>
24
+ <dd class="flex items-center mt-2 text-blue-600 text-sm dark:text-blue-400">
25
+ <div class="mr-1 h-4 w-4 i-hugeicons-hourglass"></div>
26
+ <span>{{ waitingEntries() }} parties waiting</span>
27
+ </dd>
28
+ </div>
29
+
30
+ <div class="overflow-hidden px-4 py-5 sm:p-6 bg-white dark:bg-neutral-800 rounded-lg shadow">
31
+ <dt class="font-medium text-gray-500 text-sm truncate dark:text-neutral-300">Seated Today</dt>
32
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ seatedEntries() }}</dd>
33
+ <dd class="flex items-center mt-2 text-green-600 text-sm dark:text-green-400">
34
+ <div class="mr-1 h-4 w-4 i-hugeicons-checkmark-circle-02"></div>
35
+ <span>Tables turned</span>
36
+ </dd>
37
+ </div>
38
+
39
+ <div class="overflow-hidden px-4 py-5 sm:p-6 bg-white dark:bg-neutral-800 rounded-lg shadow">
40
+ <dt class="font-medium text-gray-500 text-sm truncate dark:text-neutral-300">No Shows</dt>
41
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ noShowEntries() }}</dd>
42
+ <dd class="flex items-center mt-2 text-red-600 text-sm dark:text-red-400">
43
+ <div class="mr-1 h-4 w-4 i-hugeicons-cancel-circle"></div>
44
+ <span>Missed reservations</span>
45
+ </dd>
46
+ </div>
47
+
48
+ <div class="overflow-hidden px-4 py-5 sm:p-6 bg-white dark:bg-neutral-800 rounded-lg shadow">
49
+ <dt class="font-medium text-gray-500 text-sm truncate dark:text-neutral-300">Seating Rate</dt>
50
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ seatingRate() }}%</dd>
51
+ <dd class="flex items-center mt-2 text-purple-600 text-sm dark:text-purple-400">
52
+ <div class="mr-1 h-4 w-4 i-hugeicons-analytics-up"></div>
53
+ <span>{{ seatedEntries() }} parties seated</span>
54
+ </dd>
55
+ </div>
56
+ </dl>
57
+
58
+ <div class="grid gap-5 grid-cols-1 lg:grid-cols-2 mt-8">
59
+ <div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
60
+ <div class="p-6">
61
+ <h3 class="font-semibold leading-6 text-base text-gray-900 dark:text-white">Wait Time Trend</h3>
62
+ <div class="mt-2 h-80">
63
+ <canvas id="restaurant_trend_chart"></canvas>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
69
+ <div class="p-6">
70
+ <h3 class="font-semibold leading-6 text-base text-gray-900 dark:text-white">Party Sizes</h3>
71
+ <div class="mt-2 h-80">
72
+ <canvas id="restaurant_product_chart"></canvas>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
78
+ <div class="p-6">
79
+ <h3 class="font-semibold leading-6 text-base text-gray-900 dark:text-white">Table Preferences</h3>
80
+ <div class="mt-2 h-80">
81
+ <canvas id="restaurant_source_chart"></canvas>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
87
+ <div class="p-6">
88
+ <h3 class="font-semibold leading-6 text-base text-gray-900 dark:text-white">Current Status</h3>
89
+ <div class="mt-2 h-80">
90
+ <canvas id="restaurant_status_chart"></canvas>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mt-6">
97
+ <div class="relative max-w-sm">
98
+ <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
99
+ <div class="h-5 w-5 text-gray-400 i-hugeicons-search-01"></div>
100
+ </div>
101
+ <input x-model="searchQuery" type="text" placeholder="Search waitlist..." class="block pl-10 pr-3 py-1.5 w-full text-gray-900 sm:leading-6 sm:text-sm dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset" />
102
+ </div>
103
+
104
+ <div class="flex flex-col gap-4 sm:flex-row">
105
+ <select x-model="partySizeFilter" class="block pl-3 pr-10 py-1.5 w-full text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset">
106
+ <option value="all">All Party Sizes</option>
107
+ <option value="1-2">1-2 People</option>
108
+ <option value="3-4">3-4 People</option>
109
+ <option value="5-6">5-6 People</option>
110
+ <option value="7+">7+ People</option>
111
+ </select>
112
+
113
+ <select x-model="statusFilter" class="block pl-3 pr-10 py-1.5 w-full text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset">
114
+ <option value="all">All Statuses</option>
115
+ <option value="Waiting">Waiting</option>
116
+ <option value="Seated">Seated</option>
117
+ <option value="Cancelled">Cancelled</option>
118
+ <option value="No Show">No Show</option>
119
+ </select>
120
+
121
+ <select x-model="tablePreferenceFilter" class="block pl-3 pr-10 py-1.5 w-full text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset">
122
+ <option value="all">All Table Types</option>
123
+ <option value="Indoor">Indoor</option>
124
+ <option value="Outdoor">Outdoor</option>
125
+ <option value="Bar">Bar</option>
126
+ <option value="Booth">Booth</option>
127
+ <option value="Private">Private</option>
128
+ </select>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="flex items-center mt-4 text-gray-500 text-sm dark:text-neutral-400">
133
+ <span class="mr-2">Sort by:</span>
134
+ <button @click="toggleSort('name')" class="flex items-center mr-3 hover:text-gray-700 dark:hover:text-gray-300 {{ sortBy() === 'name' ? 'font-semibold text-blue-600 dark:text-blue-400' : '' }}">
135
+ Name
136
+ <span :show="sortBy() === 'name'" class="ml-1">
137
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
138
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
139
+ </span>
140
+ </button>
141
+ <button @click="toggleSort('checkInTime')" class="flex items-center mr-3 hover:text-gray-700 dark:hover:text-gray-300 {{ sortBy() === 'checkInTime' ? 'font-semibold text-blue-600 dark:text-blue-400' : '' }}">
142
+ Check-in Time
143
+ <span :show="sortBy() === 'checkInTime'" class="ml-1">
144
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
145
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
146
+ </span>
147
+ </button>
148
+ <button @click="toggleSort('estimatedWaitMinutes')" class="flex items-center hover:text-gray-700 dark:hover:text-gray-300 {{ sortBy() === 'estimatedWaitMinutes' ? 'font-semibold text-blue-600 dark:text-blue-400' : '' }}">
149
+ Wait Time
150
+ <span :show="sortBy() === 'estimatedWaitMinutes'" class="ml-1">
151
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
152
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
153
+ </span>
154
+ </button>
155
+ </div>
156
+
157
+ <div :show="hasSelection" class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-md">
158
+ <div class="flex items-center justify-between">
159
+ <div class="flex items-center">
160
+ <span class="font-medium text-blue-700 text-sm dark:text-blue-300">{{ selectedCount() }} parties selected</span>
161
+ </div>
162
+ <div class="flex space-x-3">
163
+ <button @click="openNotificationModal()" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md shadow-sm">
164
+ <div class="mr-1 h-4 w-4 i-hugeicons-notification-square"></div>
165
+ Send Updates
166
+ </button>
167
+ <button @click="bulkChangeStatus('Seated')" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-green-600 hover:bg-green-500 rounded-md shadow-sm">
168
+ <div class="mr-1 h-4 w-4 i-hugeicons-checkmark-circle-02"></div>
169
+ Mark as Seated
170
+ </button>
171
+ <button @click="bulkChangeStatus('No Show')" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-red-600 hover:bg-red-500 rounded-md shadow-sm">
172
+ <div class="mr-1 h-4 w-4 i-hugeicons-cancel-circle"></div>
173
+ Mark as No Show
174
+ </button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="overflow-hidden mt-6 ring-1 ring-black ring-opacity-5 sm:rounded-lg shadow">
180
+ <table class="min-w-full divide-gray-300 divide-y dark:divide-neutral-700">
181
+ <thead class="bg-gray-50 dark:bg-neutral-900">
182
+ <tr>
183
+ <th scope="col" class="relative pl-4 pr-3 py-3.5 sm:pl-6">
184
+ <input type="checkbox" @change="toggleSelectAll()" {{ selectAll() ? 'checked' : '' }} class="h-4 w-4 text-blue-600 border-gray-300 rounded dark:border-neutral-600 focus:ring-blue-600" />
185
+ </th>
186
+ <th scope="col" class="pl-4 pr-3 py-3.5 sm:pl-6 font-semibold text-gray-900 text-left text-sm dark:text-white">Party Details</th>
187
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Wait Time</th>
188
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Status</th>
189
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Check-in Time</th>
190
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Table Preference</th>
191
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Queue Position</th>
192
+ <th scope="col" class="relative pl-3 pr-4 py-3.5 sm:pr-6"><span class="sr-only">Actions</span></th>
193
+ </tr>
194
+ </thead>
195
+ <tbody class="bg-white dark:bg-neutral-800 divide-gray-200 divide-y dark:divide-neutral-700">
196
+ <tr :show="hasNoEntries" class="text-center">
197
+ <td colspan="8" class="py-10 text-gray-500 dark:text-neutral-400">No parties currently on the waitlist.</td>
198
+ </tr>
199
+ <template :for="entry in paginatedEntries">
200
+ <tr class="hover:bg-gray-50 dark:hover:bg-neutral-700">
201
+ <td class="pl-4 pr-3 py-4 sm:pl-6 text-sm whitespace-nowrap">
202
+ <input type="checkbox" @change="toggleEntrySelection({{ entry.id }})" {{ isSelected(entry.id) ? 'checked' : '' }} class="h-4 w-4 text-blue-600 border-gray-300 rounded dark:border-neutral-600 focus:ring-blue-600" />
203
+ </td>
204
+ <td class="pl-4 pr-3 py-4 sm:pl-6 text-sm whitespace-nowrap">
205
+ <div class="flex items-center">
206
+ <div>
207
+ <div class="font-medium text-gray-900 dark:text-white">{{ entry.name }} ({{ entry.partySize }} {{ entry.partySize === 1 ? 'person' : 'people' }})</div>
208
+ <div class="text-gray-500 dark:text-neutral-400">{{ entry.phone || entry.email }}</div>
209
+ </div>
210
+ </div>
211
+ </td>
212
+ <td class="px-3 py-4 text-sm whitespace-nowrap">
213
+ <div class="font-medium text-gray-900 dark:text-white">{{ entry.estimatedWaitMinutes }} min</div>
214
+ <div class="text-gray-500 dark:text-neutral-400">Quoted: {{ entry.quotedWaitMinutes }} min</div>
215
+ </td>
216
+ <td class="px-3 py-4 text-sm whitespace-nowrap">
217
+ <span class="inline-flex items-center px-2.5 py-0.5 font-medium text-xs rounded-full {{ getStatusClass(entry.status) }}">{{ entry.status }}</span>
218
+ </td>
219
+ <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatTime(entry.checkInTime) }}</td>
220
+ <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ entry.tablePreference || 'No Preference' }}</td>
221
+ <td class="px-3 py-4 text-sm whitespace-nowrap">
222
+ <span :show="{{ entry.status === 'Waiting' ? 'true' : 'false' }}" class="font-medium text-gray-900 dark:text-white">#{{ entry.queuePosition }}</span>
223
+ <span :show="{{ entry.status !== 'Waiting' ? 'true' : 'false' }}" class="text-gray-500 dark:text-neutral-400">-</span>
224
+ </td>
225
+ <td class="relative pl-3 pr-4 py-4 sm:pr-6 font-medium text-right text-sm whitespace-nowrap">
226
+ <div class="flex justify-end space-x-2">
227
+ <button @click="openEditEntryModal({{ entry.id }})" class="text-gray-400 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 transition-colors">
228
+ <div class="h-5 w-5 i-hugeicons-edit-01"></div>
229
+ <span class="sr-only">Edit</span>
230
+ </button>
231
+ <button :show="{{ entry.status === 'Waiting' ? 'true' : 'false' }}" @click="markAsSeated({{ entry.id }})" class="text-gray-400 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 transition-colors">
232
+ <div class="h-5 w-5 i-hugeicons-checkmark-circle-02"></div>
233
+ <span class="sr-only">Seat</span>
234
+ </button>
235
+ <button :show="{{ entry.status === 'Waiting' ? 'true' : 'false' }}" @click="markAsNoShow({{ entry.id }})" class="text-gray-400 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 transition-colors">
236
+ <div class="h-5 w-5 i-hugeicons-cancel-circle"></div>
237
+ <span class="sr-only">No Show</span>
238
+ </button>
239
+ </div>
240
+ </td>
241
+ </tr>
242
+ </template>
243
+ </tbody>
244
+ </table>
245
+ </div>
246
+
247
+ <div class="flex items-center justify-between mt-5">
248
+ <div class="flex sm:hidden flex-1 justify-between">
249
+ <button @click="prevPage()" class="{{ currentPage() === 1 ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-50 dark:hover:bg-neutral-700' }} relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white">Previous</button>
250
+ <button @click="nextPage()" class="{{ currentPage() === totalPages() ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-50 dark:hover:bg-neutral-700' }} relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white">Next</button>
251
+ </div>
252
+ <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
253
+ <div>
254
+ <p class="text-gray-700 text-sm dark:text-neutral-300">
255
+ Showing <span class="font-medium">{{ rangeStart() }}</span> to <span class="font-medium">{{ rangeEnd() }}</span> of <span class="font-medium">{{ totalFiltered() }}</span> results
256
+ </p>
257
+ </div>
258
+ <div>
259
+ <nav class="inline-flex -space-x-px rounded-md shadow-sm isolate" aria-label="Pagination">
260
+ <button @click="goToPage(currentPage() - 1)" class="{{ currentPage() === 1 ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-50 dark:hover:bg-neutral-700' }} relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 dark:ring-neutral-600">
261
+ <span class="sr-only">Previous</span>
262
+ <div class="h-5 w-5 i-hugeicons-arrow-left-01"></div>
263
+ </button>
264
+ <template :for="page in pageNumbers">
265
+ <button @click="goToPage({{ page }})" class="{{ page === currentPage() ? 'relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600' : 'relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 dark:text-white dark:ring-neutral-600 dark:hover:bg-neutral-700' }} focus:z-20">{{ page }}</button>
266
+ </template>
267
+ <button @click="goToPage(currentPage() + 1)" class="{{ currentPage() === totalPages() ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-50 dark:hover:bg-neutral-700' }} relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 dark:ring-neutral-600">
268
+ <span class="sr-only">Next</span>
269
+ <div class="h-5 w-5 i-hugeicons-arrow-right-01"></div>
270
+ </button>
271
+ </nav>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <!-- Add/Edit Entry Modal -->
279
+ <div :show="showEntryModal" class="fixed inset-0 overflow-y-auto z-10">
280
+ <div class="flex items-end justify-center sm:items-center p-4 sm:p-0 min-h-full text-center">
281
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="closeEntryModal()"></div>
282
+ <div class="overflow-hidden relative pb-4 pt-5 px-4 sm:my-8 sm:p-6 sm:max-w-lg sm:w-full text-left bg-white dark:bg-neutral-800 rounded-lg shadow-xl transition-all transform">
283
+ <div>
284
+ <div class="mt-3 sm:mt-5 text-center">
285
+ <h3 class="font-semibold leading-6 text-gray-900 text-lg dark:text-white">{{ entryModalTitle() }}</h3>
286
+ <div class="mt-2">
287
+ <p class="text-gray-500 text-sm dark:text-neutral-400">{{ entryModalDescription() }}</p>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <div class="mt-5 sm:mt-6">
293
+ <form @submit.prevent="saveEntry()">
294
+ <div class="grid gap-x-4 gap-y-6 grid-cols-1 sm:grid-cols-6">
295
+ <div class="sm:col-span-3">
296
+ <label for="name" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Name</label>
297
+ <div class="mt-1">
298
+ <input id="name" x-model="entryName" type="text" required class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm" />
299
+ </div>
300
+ </div>
301
+
302
+ <div class="sm:col-span-3">
303
+ <label for="email" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Email</label>
304
+ <div class="mt-1">
305
+ <input id="email" x-model="entryEmail" type="email" required class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm" />
306
+ </div>
307
+ </div>
308
+
309
+ <div class="sm:col-span-3">
310
+ <label for="phone" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Phone (optional)</label>
311
+ <div class="mt-1">
312
+ <input id="phone" x-model="entryPhone" type="tel" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm" />
313
+ </div>
314
+ </div>
315
+
316
+ <div class="sm:col-span-3">
317
+ <label for="partySize" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Party Size</label>
318
+ <div class="mt-1">
319
+ <input id="partySize" x-model.number="entryPartySize" type="number" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm" />
320
+ </div>
321
+ </div>
322
+
323
+ <div class="sm:col-span-3">
324
+ <label for="notificationPreference" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Notification Preference</label>
325
+ <div class="mt-1">
326
+ <select id="notificationPreference" x-model="entryNotificationPreference" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm">
327
+ <option value="sms">sms</option>
328
+ <option value="email">email</option>
329
+ <option value="both">both</option>
330
+ </select>
331
+ </div>
332
+ </div>
333
+
334
+ <div class="sm:col-span-3">
335
+ <label for="tablePreference" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Table Preference</label>
336
+ <div class="mt-1">
337
+ <select id="tablePreference" x-model="entryTablePreference" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm">
338
+ <option value="No Preference">No Preference</option>
339
+ <option value="Indoor">Indoor</option>
340
+ <option value="Outdoor">Outdoor</option>
341
+ <option value="Bar">Bar</option>
342
+ <option value="Booth">Booth</option>
343
+ <option value="Private">Private</option>
344
+ </select>
345
+ </div>
346
+ </div>
347
+
348
+ <div class="sm:col-span-6">
349
+ <label for="specialRequests" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Special Requests (optional)</label>
350
+ <div class="mt-1">
351
+ <textarea id="specialRequests" x-model="entrySpecialRequests" rows="3" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm"></textarea>
352
+ </div>
353
+ </div>
354
+
355
+ <div class="sm:col-span-6">
356
+ <label for="notes" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Notes (optional)</label>
357
+ <div class="mt-1">
358
+ <textarea id="notes" x-model="entryNotes" rows="3" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm"></textarea>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <div class="sm:grid sm:gap-3 sm:grid-cols-2 sm:grid-flow-row-dense mt-5 sm:mt-6">
364
+ <button type="submit" class="inline-flex justify-center sm:col-start-2 px-3 py-2 w-full font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 shadow-sm">{{ entryModalSubmitLabel() }}</button>
365
+ <button type="button" @click="closeEntryModal()" class="inline-flex justify-center sm:col-start-1 mt-3 px-3 py-2 sm:mt-0 w-full font-semibold text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-700 hover:bg-gray-50 dark:hover:bg-neutral-600 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 shadow-sm">Cancel</button>
366
+ </div>
367
+ </form>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Notification Modal -->
374
+ <div :show="showNotificationModal" class="fixed inset-0 overflow-y-auto z-10">
375
+ <div class="flex items-end justify-center sm:items-center p-4 sm:p-0 min-h-full text-center">
376
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="closeNotificationModal()"></div>
377
+ <div class="overflow-hidden relative pb-4 pt-5 px-4 sm:my-8 sm:p-6 sm:max-w-lg sm:w-full text-left bg-white dark:bg-neutral-800 rounded-lg shadow-xl transition-all transform">
378
+ <div>
379
+ <div class="mt-3 sm:mt-5 text-center">
380
+ <h3 class="font-semibold leading-6 text-gray-900 text-lg dark:text-white">Send Notification</h3>
381
+ <div class="mt-2">
382
+ <p class="text-gray-500 text-sm dark:text-neutral-400">Subject:</p>
383
+ <input x-model="notificationSubject" type="text" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm" />
384
+ <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Message:</p>
385
+ <textarea x-model="notificationMessage" rows="3" class="block w-full sm:text-sm dark:text-white dark:bg-neutral-700 border-gray-300 rounded-md dark:border-neutral-600 focus:border-blue-500 focus:ring-blue-500 shadow-sm"></textarea>
386
+ </div>
387
+ </div>
388
+ </div>
389
+
390
+ <div class="mt-5 sm:mt-6">
391
+ <button @click="sendNotifications()" class="inline-flex justify-center px-3 py-2 w-full font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 shadow-sm">Send Notification</button>
392
+ </div>
393
+ </div>
394
+ </PageLayout>
395
+ <script client>
396
+
397
+ interface WaitlistEntry {
398
+ id: number
399
+ name: string
400
+ email: string
401
+ phone: string | null
402
+ partySize: number
403
+ estimatedWaitMinutes: number
404
+ quotedWaitMinutes: number
405
+ queuePosition: number
406
+ notificationPreference: 'sms' | 'email' | 'both'
407
+ notes: string | null
408
+ status: string
409
+ dateAdded: string
410
+ notified: boolean
411
+ priority: number
412
+ lastNotified: string | null
413
+ tablePreference: string | null
414
+ specialRequests: string | null
415
+ checkInTime: string
416
+ estimatedSeatingTime: string | null
417
+ actualSeatingTime: string | null
418
+ }
419
+
420
+ interface ChartLike { destroy: () => void, update: () => void, data: { labels: string[], datasets: Array<{ data: number[] }> } }
421
+ type ChartCtor = new (canvas: HTMLElement, config: { type: string, data: unknown, options?: unknown }) => ChartLike
422
+
423
+ const statuses = ['all', 'Waiting', 'Seated', 'Cancelled', 'No Show']
424
+ const tablePreferences = ['No Preference', 'Indoor', 'Outdoor', 'Bar', 'Booth', 'Private']
425
+
426
+ function isoNow(): string {
427
+ return new Date().toISOString()
428
+ }
429
+
430
+ const waitlistEntries = state<WaitlistEntry[]>([
431
+ { id: 1, name: 'John Smith', email: 'john.smith@example.com', phone: '+1 (555) 123-4567', partySize: 4, estimatedWaitMinutes: 25, quotedWaitMinutes: 30, queuePosition: 1, notificationPreference: 'sms', notes: 'Birthday celebration', status: 'Waiting', dateAdded: '2024-01-10', notified: false, priority: 2, lastNotified: null, tablePreference: 'Booth', specialRequests: 'High chair needed', checkInTime: isoNow(), estimatedSeatingTime: null, actualSeatingTime: null },
432
+ { id: 2, name: 'Emily Johnson', email: 'emily.j@example.com', phone: '+1 (555) 987-6543', partySize: 2, estimatedWaitMinutes: 15, quotedWaitMinutes: 18, queuePosition: 2, notificationPreference: 'email', notes: null, status: 'Waiting', dateAdded: '2024-01-12', notified: false, priority: 1, lastNotified: null, tablePreference: null, specialRequests: null, checkInTime: isoNow(), estimatedSeatingTime: null, actualSeatingTime: null },
433
+ { id: 3, name: 'Michael Brown', email: 'michael.b@example.com', phone: null, partySize: 3, estimatedWaitMinutes: 20, quotedWaitMinutes: 24, queuePosition: 3, notificationPreference: 'both', notes: 'VIP customer, notify immediately when available', status: 'Seated', dateAdded: '2024-01-15', notified: true, priority: 3, lastNotified: '2024-01-15T12:30:00', tablePreference: 'Indoor', specialRequests: null, checkInTime: '2024-01-15T12:00:00', estimatedSeatingTime: '2024-01-15T12:30:00', actualSeatingTime: '2024-01-15T12:30:00' },
434
+ { id: 4, name: 'Sarah Wilson', email: 'sarah.w@example.com', phone: '+1 (555) 456-7890', partySize: 2, estimatedWaitMinutes: 10, quotedWaitMinutes: 12, queuePosition: 4, notificationPreference: 'sms', notes: 'Wants to be notified when back in stock', status: 'Waiting', dateAdded: '2024-01-18', notified: false, priority: 2, lastNotified: null, tablePreference: null, specialRequests: null, checkInTime: isoNow(), estimatedSeatingTime: null, actualSeatingTime: null },
435
+ { id: 5, name: 'David Lee', email: 'david.l@example.com', phone: '+1 (555) 234-5678', partySize: 3, estimatedWaitMinutes: 18, quotedWaitMinutes: 21, queuePosition: 5, notificationPreference: 'email', notes: 'Requested notification via SMS', status: 'Seated', dateAdded: '2024-01-20', notified: true, priority: 2, lastNotified: '2024-01-20T12:30:00', tablePreference: 'Bar', specialRequests: 'No onions please', checkInTime: '2024-01-20T12:00:00', estimatedSeatingTime: '2024-01-20T12:30:00', actualSeatingTime: '2024-01-20T12:30:00' },
436
+ { id: 6, name: 'Jennifer Martinez', email: 'jennifer.m@example.com', phone: null, partySize: 2, estimatedWaitMinutes: 12, quotedWaitMinutes: 14, queuePosition: 6, notificationPreference: 'both', notes: null, status: 'Waiting', dateAdded: '2024-01-22', notified: false, priority: 1, lastNotified: null, tablePreference: null, specialRequests: null, checkInTime: isoNow(), estimatedSeatingTime: null, actualSeatingTime: null },
437
+ { id: 7, name: 'Robert Taylor', email: 'robert.t@example.com', phone: '+1 (555) 876-5432', partySize: 4, estimatedWaitMinutes: 25, quotedWaitMinutes: 30, queuePosition: 7, notificationPreference: 'sms', notes: 'Wants to purchase multiple units when available', status: 'Waiting', dateAdded: '2024-01-25', notified: false, priority: 3, lastNotified: null, tablePreference: 'Booth', specialRequests: null, checkInTime: isoNow(), estimatedSeatingTime: null, actualSeatingTime: null },
438
+ { id: 8, name: 'Lisa Anderson', email: 'lisa.a@example.com', phone: '+1 (555) 345-6789', partySize: 2, estimatedWaitMinutes: 10, quotedWaitMinutes: 12, queuePosition: 8, notificationPreference: 'email', notes: null, status: 'Seated', dateAdded: '2024-01-28', notified: true, priority: 2, lastNotified: '2024-01-28T12:30:00', tablePreference: 'Indoor', specialRequests: null, checkInTime: '2024-01-28T12:00:00', estimatedSeatingTime: '2024-01-28T12:30:00', actualSeatingTime: '2024-01-28T12:30:00' },
439
+ ])
440
+
441
+ const searchQuery = state('')
442
+ const statusFilter = state('all')
443
+ const partySizeFilter = state('all')
444
+ const tablePreferenceFilter = state('all')
445
+ const sortBy = state('checkInTime')
446
+ const sortOrder = state<'asc' | 'desc'>('asc')
447
+
448
+ const itemsPerPage = 10
449
+ const currentPage = state(1)
450
+
451
+ const showEntryModal = state(false)
452
+ const isEditMode = state(false)
453
+ const editingEntryId = state<number | null>(null)
454
+
455
+ const entryName = state('')
456
+ const entryEmail = state('')
457
+ const entryPhone = state('')
458
+ const entryPartySize = state(2)
459
+ const entryNotificationPreference = state<'sms' | 'email' | 'both'>('sms')
460
+ const entryTablePreference = state('No Preference')
461
+ const entrySpecialRequests = state('')
462
+ const entryNotes = state('')
463
+ const entryPriority = state(1)
464
+
465
+ const showNotificationModal = state(false)
466
+ const notificationSubject = state('Table is Ready')
467
+ const notificationMessage = state('Good news! Your table is ready. Please proceed to the host stand.')
468
+
469
+ const selectedEntries = state<number[]>([])
470
+ const selectAll = state(false)
471
+
472
+ const trendData = state<Array<{ date: string, count: number }>>(generateTrendData())
473
+
474
+ let charts: ChartLike[] = []
475
+
476
+ const totalEntries = derived(() => waitlistEntries().length)
477
+ const waitingEntries = derived(() => waitlistEntries().filter(e => e.status === 'Waiting').length)
478
+ const seatedEntries = derived(() => waitlistEntries().filter(e => e.status === 'Seated').length)
479
+ const noShowEntries = derived(() => waitlistEntries().filter(e => e.status === 'No Show').length)
480
+ const seatingRate = derived(() => Math.round((seatedEntries() / Math.max(1, totalEntries())) * 100))
481
+
482
+ const averageWait = derived(() => {
483
+ const completed = waitlistEntries().filter(e => e.status === 'Seated' && e.actualSeatingTime !== null)
484
+ if (completed.length === 0)
485
+ return 0
486
+ const totals = completed.map((e) => {
487
+ const checkIn = new Date(e.checkInTime).getTime()
488
+ const seating = new Date(e.actualSeatingTime as string).getTime()
489
+ return (seating - checkIn) / 60000
490
+ })
491
+ return Math.round(totals.reduce((a, b) => a + b, 0) / totals.length)
492
+ })
493
+
494
+ const filteredEntries = derived(() => {
495
+ const list = waitlistEntries()
496
+ const q = searchQuery().toLowerCase()
497
+ const sf = statusFilter()
498
+ const pf = partySizeFilter()
499
+ const tf = tablePreferenceFilter()
500
+ return list.filter((entry) => {
501
+ const matchesSearch = q === ''
502
+ || entry.name.toLowerCase().includes(q)
503
+ || (entry.phone !== null && entry.phone.includes(searchQuery()))
504
+ || entry.email.toLowerCase().includes(q)
505
+
506
+ let matchesPartySize = true
507
+ if (pf !== 'all') {
508
+ const [minStr, maxStr] = pf.split('-')
509
+ if (!minStr) {
510
+ matchesPartySize = false
511
+ }
512
+ else {
513
+ const min = Number.parseInt(minStr, 10)
514
+ if (maxStr) {
515
+ const max = Number.parseInt(maxStr, 10)
516
+ matchesPartySize = entry.partySize >= min && entry.partySize <= max
517
+ }
518
+ else {
519
+ matchesPartySize = entry.partySize >= min
520
+ }
521
+ }
522
+ }
523
+
524
+ const matchesStatus = sf === 'all' || entry.status === sf
525
+ const matchesTablePreference = tf === 'all' || entry.tablePreference === tf
526
+ return matchesSearch && matchesPartySize && matchesStatus && matchesTablePreference
527
+ })
528
+ })
529
+
530
+ const sortedEntries = derived(() => {
531
+ const list = [...filteredEntries()]
532
+ const sb = sortBy()
533
+ const so = sortOrder()
534
+ return list.sort((a, b) => {
535
+ if (sb === 'name')
536
+ return so === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
537
+ if (sb === 'checkInTime')
538
+ return so === 'asc'
539
+ ? new Date(a.checkInTime).getTime() - new Date(b.checkInTime).getTime()
540
+ : new Date(b.checkInTime).getTime() - new Date(a.checkInTime).getTime()
541
+ if (sb === 'estimatedWaitMinutes')
542
+ return so === 'asc' ? a.estimatedWaitMinutes - b.estimatedWaitMinutes : b.estimatedWaitMinutes - a.estimatedWaitMinutes
543
+ return 0
544
+ })
545
+ })
546
+
547
+ const paginatedEntries = derived(() => {
548
+ const start = (currentPage() - 1) * itemsPerPage
549
+ return sortedEntries().slice(start, start + itemsPerPage)
550
+ })
551
+
552
+ const totalPages = derived(() => Math.max(1, Math.ceil(sortedEntries().length / itemsPerPage)))
553
+ const pageNumbers = derived(() => Array.from({ length: totalPages() }, (_, i) => i + 1))
554
+ const totalFiltered = derived(() => filteredEntries().length)
555
+ const rangeStart = derived(() => totalFiltered() === 0 ? 0 : (currentPage() - 1) * itemsPerPage + 1)
556
+ const rangeEnd = derived(() => Math.min(currentPage() * itemsPerPage, totalFiltered()))
557
+ const hasNoEntries = derived(() => paginatedEntries().length === 0)
558
+ const hasSelection = derived(() => selectedEntries().length > 0)
559
+ const selectedCount = derived(() => selectedEntries().length)
560
+
561
+ function entryModalTitle(): string { return isEditMode() ? 'Edit Waitlist Entry' : 'Add to Waitlist' }
562
+ function entryModalDescription(): string { return isEditMode() ? 'Update the details for this waitlist entry.' : 'Fill in the details to add a new party to the waitlist.' }
563
+ function entryModalSubmitLabel(): string { return isEditMode() ? 'Save Changes' : 'Add to Waitlist' }
564
+
565
+ function isSelected(id: number): boolean { return selectedEntries().includes(id) }
566
+
567
+ function getStatusClass(status: string): string {
568
+ switch (status) {
569
+ case 'Waiting': return 'bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400'
570
+ case 'Seated': return 'bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20 dark:bg-green-900/30 dark:text-green-400'
571
+ default: return 'bg-gray-50 text-gray-700 ring-1 ring-inset ring-gray-600/20 dark:bg-neutral-900/30 dark:text-neutral-400'
572
+ }
573
+ }
574
+
575
+ function formatTime(isoString: string): string {
576
+ return new Date(isoString).toLocaleTimeString()
577
+ }
578
+
579
+ function calculateEstimatedWait(partySize: number): number {
580
+ const baseWait = 15
581
+ const queueFactor = waitlistEntries().filter(e => e.status === 'Waiting').length * 5
582
+ const partySizeFactor = Math.ceil(partySize / 2) * 5
583
+ return baseWait + queueFactor + partySizeFactor
584
+ }
585
+
586
+ function recomputeQueuePositions(list: WaitlistEntry[]): WaitlistEntry[] {
587
+ const waiting = list
588
+ .filter(e => e.status === 'Waiting')
589
+ .sort((a, b) => new Date(a.checkInTime).getTime() - new Date(b.checkInTime).getTime())
590
+ const positions = new Map<number, { position: number, estimated: number, estimatedSeatingTime: string }>()
591
+ waiting.forEach((entry, index) => {
592
+ const estimated = calculateEstimatedWait(entry.partySize)
593
+ positions.set(entry.id, {
594
+ position: index + 1,
595
+ estimated,
596
+ estimatedSeatingTime: new Date(new Date(entry.checkInTime).getTime() + estimated * 60000).toISOString(),
597
+ })
598
+ })
599
+ return list.map((entry) => {
600
+ const upd = positions.get(entry.id)
601
+ if (!upd)
602
+ return entry
603
+ return { ...entry, queuePosition: upd.position, estimatedWaitMinutes: upd.estimated, estimatedSeatingTime: upd.estimatedSeatingTime }
604
+ })
605
+ }
606
+
607
+ function toggleSort(field: string): void {
608
+ if (sortBy() === field)
609
+ sortOrder.set(sortOrder() === 'asc' ? 'desc' : 'asc')
610
+ else {
611
+ sortBy.set(field)
612
+ sortOrder.set('asc')
613
+ }
614
+ }
615
+
616
+ function nextPage(): void {
617
+ if (currentPage() < totalPages())
618
+ currentPage.update(p => p + 1)
619
+ }
620
+
621
+ function prevPage(): void {
622
+ if (currentPage() > 1)
623
+ currentPage.update(p => p - 1)
624
+ }
625
+
626
+ function goToPage(page: number): void {
627
+ if (page >= 1 && page <= totalPages())
628
+ currentPage.set(page)
629
+ }
630
+
631
+ function resetForm(): void {
632
+ entryName.set('')
633
+ entryEmail.set('')
634
+ entryPhone.set('')
635
+ entryPartySize.set(2)
636
+ entryNotificationPreference.set('sms')
637
+ entryTablePreference.set('No Preference')
638
+ entrySpecialRequests.set('')
639
+ entryNotes.set('')
640
+ entryPriority.set(1)
641
+ }
642
+
643
+ function openAddEntryModal(): void {
644
+ isEditMode.set(false)
645
+ editingEntryId.set(null)
646
+ resetForm()
647
+ showEntryModal.set(true)
648
+ }
649
+
650
+ function openEditEntryModal(id: number): void {
651
+ const target = waitlistEntries().find(e => e.id === id)
652
+ if (!target)
653
+ return
654
+ isEditMode.set(true)
655
+ editingEntryId.set(id)
656
+ entryName.set(target.name)
657
+ entryEmail.set(target.email)
658
+ entryPhone.set(target.phone ?? '')
659
+ entryPartySize.set(target.partySize)
660
+ entryNotificationPreference.set(target.notificationPreference)
661
+ entryTablePreference.set(target.tablePreference ?? 'No Preference')
662
+ entrySpecialRequests.set(target.specialRequests ?? '')
663
+ entryNotes.set(target.notes ?? '')
664
+ entryPriority.set(target.priority)
665
+ showEntryModal.set(true)
666
+ }
667
+
668
+ function closeEntryModal(): void {
669
+ showEntryModal.set(false)
670
+ editingEntryId.set(null)
671
+ }
672
+
673
+ function saveEntry(): void {
674
+ const now = new Date()
675
+ const editId = editingEntryId()
676
+ const list = waitlistEntries()
677
+ const id = isEditMode() && editId !== null ? editId : Math.max(0, ...list.map(e => e.id)) + 1
678
+ const partySize = entryPartySize()
679
+ const estimated = calculateEstimatedWait(partySize)
680
+ const tablePref = entryTablePreference()
681
+ const entry: WaitlistEntry = {
682
+ id,
683
+ name: entryName(),
684
+ email: entryEmail(),
685
+ phone: entryPhone() || null,
686
+ partySize,
687
+ estimatedWaitMinutes: estimated,
688
+ quotedWaitMinutes: Math.ceil(estimated * 1.2),
689
+ queuePosition: list.filter(e => e.status === 'Waiting').length + 1,
690
+ notificationPreference: entryNotificationPreference(),
691
+ notes: entryNotes() || null,
692
+ status: 'Waiting',
693
+ dateAdded: now.toISOString().split('T')[0] ?? '',
694
+ notified: false,
695
+ priority: entryPriority(),
696
+ lastNotified: null,
697
+ tablePreference: tablePref === 'No Preference' ? null : tablePref,
698
+ specialRequests: entrySpecialRequests() || null,
699
+ checkInTime: now.toISOString(),
700
+ estimatedSeatingTime: null,
701
+ actualSeatingTime: null,
702
+ }
703
+
704
+ const next = isEditMode() && editId !== null
705
+ ? list.map(e => e.id === editId ? entry : e)
706
+ : [...list, entry]
707
+ waitlistEntries.set(recomputeQueuePositions(next))
708
+
709
+ closeEntryModal()
710
+ refreshCharts()
711
+ }
712
+
713
+ function deleteEntry(id: number): void {
714
+ // eslint-disable-next-line no-alert
715
+ if (!confirm('Are you sure you want to delete this waitlist entry?'))
716
+ return
717
+ waitlistEntries.set(recomputeQueuePositions(waitlistEntries().filter(e => e.id !== id)))
718
+ refreshCharts()
719
+ }
720
+
721
+ function markAsSeated(id: number): void {
722
+ const next = waitlistEntries().map(e => e.id === id
723
+ ? { ...e, status: 'Seated', actualSeatingTime: new Date().toISOString() }
724
+ : e)
725
+ waitlistEntries.set(recomputeQueuePositions(next))
726
+ refreshCharts()
727
+ }
728
+
729
+ function markAsNoShow(id: number): void {
730
+ const next = waitlistEntries().map(e => e.id === id ? { ...e, status: 'No Show' } : e)
731
+ waitlistEntries.set(recomputeQueuePositions(next))
732
+ refreshCharts()
733
+ }
734
+
735
+ function toggleSelectAll(): void {
736
+ const next = !selectAll()
737
+ selectAll.set(next)
738
+ if (next)
739
+ selectedEntries.set(paginatedEntries().map(entry => entry.id))
740
+ else
741
+ selectedEntries.set([])
742
+ }
743
+
744
+ function toggleEntrySelection(id: number): void {
745
+ const list = selectedEntries()
746
+ if (list.includes(id))
747
+ selectedEntries.set(list.filter(x => x !== id))
748
+ else
749
+ selectedEntries.set([...list, id])
750
+
751
+ const visible = paginatedEntries()
752
+ selectAll.set(visible.length > 0 && visible.every(entry => selectedEntries().includes(entry.id)))
753
+ }
754
+
755
+ function openNotificationModal(): void {
756
+ if (selectedEntries().length === 0) {
757
+ // eslint-disable-next-line no-alert
758
+ alert('Please select at least one waitlist entry to notify')
759
+ return
760
+ }
761
+ notificationSubject.set('Table is Ready')
762
+ notificationMessage.set('Good news! Your table is ready. Please proceed to the host stand.')
763
+ showNotificationModal.set(true)
764
+ }
765
+
766
+ function closeNotificationModal(): void {
767
+ showNotificationModal.set(false)
768
+ }
769
+
770
+ function sendNotifications(): void {
771
+ const ids = new Set(selectedEntries())
772
+ const next = waitlistEntries().map(e => ids.has(e.id) ? { ...e, notified: true, status: 'Seated' } : e)
773
+ waitlistEntries.set(recomputeQueuePositions(next))
774
+ const count = selectedEntries().length
775
+ selectedEntries.set([])
776
+ selectAll.set(false)
777
+ showNotificationModal.set(false)
778
+ // eslint-disable-next-line no-alert
779
+ alert(`Notifications sent to ${count} customers`)
780
+ refreshCharts()
781
+ }
782
+
783
+ function exportWaitlist(): void {
784
+ // eslint-disable-next-line no-alert
785
+ alert('Waitlist data exported successfully')
786
+ }
787
+
788
+ function bulkChangeStatus(status: string): void {
789
+ if (selectedEntries().length === 0) {
790
+ // eslint-disable-next-line no-alert
791
+ alert('Please select at least one waitlist entry')
792
+ return
793
+ }
794
+ const ids = new Set(selectedEntries())
795
+ const next = waitlistEntries().map((e) => {
796
+ if (!ids.has(e.id))
797
+ return e
798
+ if (status === 'Seated')
799
+ return { ...e, status, notified: true, actualSeatingTime: e.actualSeatingTime ?? new Date().toISOString() }
800
+ return { ...e, status }
801
+ })
802
+ waitlistEntries.set(recomputeQueuePositions(next))
803
+ selectedEntries.set([])
804
+ selectAll.set(false)
805
+ refreshCharts()
806
+ }
807
+
808
+ function bulkDelete(): void {
809
+ if (selectedEntries().length === 0) {
810
+ // eslint-disable-next-line no-alert
811
+ alert('Please select at least one waitlist entry')
812
+ return
813
+ }
814
+ // eslint-disable-next-line no-alert
815
+ if (!confirm(`Are you sure you want to delete ${selectedEntries().length} waitlist entries?`))
816
+ return
817
+ const ids = new Set(selectedEntries())
818
+ waitlistEntries.set(recomputeQueuePositions(waitlistEntries().filter(e => !ids.has(e.id))))
819
+ selectedEntries.set([])
820
+ selectAll.set(false)
821
+ refreshCharts()
822
+ }
823
+
824
+ function generateTrendData(): Array<{ date: string, count: number }> {
825
+ const data: Array<{ date: string, count: number }> = []
826
+ const today = new Date()
827
+ for (let i = 29; i >= 0; i--) {
828
+ const d = new Date(today)
829
+ d.setDate(d.getDate() - i)
830
+ data.push({ date: d.toISOString().split('T')[0] ?? '', count: Math.floor(Math.random() * 5) })
831
+ }
832
+ return data
833
+ }
834
+
835
+ const chartColors = {
836
+ blue: { fill: 'rgba(59, 130, 246, 0.2)', stroke: 'rgba(59, 130, 246, 1)', point: 'rgba(59, 130, 246, 1)' },
837
+ green: { fill: 'rgba(16, 185, 129, 0.2)', stroke: 'rgba(16, 185, 129, 1)', point: 'rgba(16, 185, 129, 1)' },
838
+ purple: { fill: 'rgba(139, 92, 246, 0.2)', stroke: 'rgba(139, 92, 246, 1)', point: 'rgba(139, 92, 246, 1)' },
839
+ yellow: { fill: 'rgba(245, 158, 11, 0.2)', stroke: 'rgba(245, 158, 11, 1)', point: 'rgba(245, 158, 11, 1)' },
840
+ red: { fill: 'rgba(239, 68, 68, 0.2)', stroke: 'rgba(239, 68, 68, 1)', point: 'rgba(239, 68, 68, 1)' },
841
+ }
842
+
843
+ const baseChartOptions = {
844
+ responsive: true,
845
+ maintainAspectRatio: false,
846
+ plugins: {
847
+ legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 20, font: { size: 12 } } },
848
+ title: { display: false },
849
+ },
850
+ }
851
+
852
+ const baseChartOptionsWithScales = {
853
+ ...baseChartOptions,
854
+ scales: {
855
+ y: { beginAtZero: true, grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }, ticks: { precision: 0 } },
856
+ x: { grid: { display: false } },
857
+ },
858
+ }
859
+
860
+ function partySizeCounts(): number[] {
861
+ const list = waitlistEntries()
862
+ return [1, 2, 3, 4, 5, 6, 7].map((size) => {
863
+ if (size === 7)
864
+ return list.filter(e => e.partySize >= 7).length
865
+ return list.filter(e => e.partySize === size).length
866
+ })
867
+ }
868
+
869
+ function partySizeLabels(): string[] {
870
+ return [1, 2, 3, 4, 5, 6, '7+'].map(size => `${size} ${size === 1 ? 'person' : 'people'}`)
871
+ }
872
+
873
+ function tablePreferenceCounts(): number[] {
874
+ const list = waitlistEntries()
875
+ return tablePreferences.slice(1).map(pref => list.filter(e => e.tablePreference === pref).length)
876
+ }
877
+
878
+ function statusCounts(): number[] {
879
+ const list = waitlistEntries()
880
+ return statuses.slice(1).map(status => list.filter(e => e.status === status).length)
881
+ }
882
+
883
+ function refreshCharts(): void {
884
+ if (charts.length !== 4)
885
+ return
886
+ const [trend, product, source, status] = charts
887
+ if (trend) {
888
+ trend.data.labels = trendData().map(d => d.date)
889
+ if (trend.data.datasets[0])
890
+ trend.data.datasets[0].data = trendData().map(d => d.count)
891
+ trend.update()
892
+ }
893
+ if (product) {
894
+ product.data.labels = partySizeLabels()
895
+ if (product.data.datasets[0])
896
+ product.data.datasets[0].data = partySizeCounts()
897
+ product.update()
898
+ }
899
+ if (source) {
900
+ source.data.labels = tablePreferences.slice(1)
901
+ if (source.data.datasets[0])
902
+ source.data.datasets[0].data = tablePreferenceCounts()
903
+ source.update()
904
+ }
905
+ if (status) {
906
+ status.data.labels = statuses.slice(1)
907
+ if (status.data.datasets[0])
908
+ status.data.datasets[0].data = statusCounts()
909
+ status.update()
910
+ }
911
+ }
912
+
913
+ onMount(async () => {
914
+ const { Chart, registerables } = await import('@stacksjs/charts')
915
+ Chart.register(...registerables)
916
+ const Ctor = Chart as unknown as ChartCtor
917
+
918
+ const trend = useChart({
919
+ Chart: Ctor,
920
+ id: 'restaurant_trend_chart',
921
+ type: 'line',
922
+ data: {
923
+ labels: trendData().map(d => d.date),
924
+ datasets: [{
925
+ label: 'New Waitlist Entries',
926
+ data: trendData().map(d => d.count),
927
+ backgroundColor: chartColors.blue.fill,
928
+ borderColor: chartColors.blue.stroke,
929
+ borderWidth: 2,
930
+ pointBackgroundColor: chartColors.blue.point,
931
+ tension: 0.4,
932
+ fill: true,
933
+ pointRadius: 4,
934
+ pointHoverRadius: 6,
935
+ }],
936
+ },
937
+ options: {
938
+ ...baseChartOptionsWithScales,
939
+ plugins: { legend: { display: false }, title: { display: false } },
940
+ },
941
+ })
942
+
943
+ const product = useChart({
944
+ Chart: Ctor,
945
+ id: 'restaurant_product_chart',
946
+ type: 'bar',
947
+ data: {
948
+ labels: partySizeLabels(),
949
+ datasets: [{
950
+ label: 'Number of Parties',
951
+ data: partySizeCounts(),
952
+ backgroundColor: chartColors.blue.fill,
953
+ borderColor: chartColors.blue.stroke,
954
+ borderWidth: 2,
955
+ borderRadius: 6,
956
+ maxBarThickness: 40,
957
+ barPercentage: 0.5,
958
+ }],
959
+ },
960
+ options: {
961
+ ...baseChartOptionsWithScales,
962
+ plugins: { legend: { display: false }, title: { display: false } },
963
+ },
964
+ })
965
+
966
+ const source = useChart({
967
+ Chart: Ctor,
968
+ id: 'restaurant_source_chart',
969
+ type: 'doughnut',
970
+ data: {
971
+ labels: tablePreferences.slice(1),
972
+ datasets: [{
973
+ data: tablePreferenceCounts(),
974
+ backgroundColor: [chartColors.blue.fill, chartColors.green.fill, chartColors.purple.fill, chartColors.yellow.fill, chartColors.red.fill],
975
+ borderColor: [chartColors.blue.stroke, chartColors.green.stroke, chartColors.purple.stroke, chartColors.yellow.stroke, chartColors.red.stroke],
976
+ borderWidth: 2,
977
+ hoverOffset: 4,
978
+ spacing: 2,
979
+ }],
980
+ },
981
+ options: {
982
+ ...baseChartOptions,
983
+ cutout: '75%',
984
+ plugins: {
985
+ legend: { position: 'right', labels: { usePointStyle: true, padding: 20, font: { size: 11 } } },
986
+ title: { display: false },
987
+ },
988
+ },
989
+ })
990
+
991
+ const status = useChart({
992
+ Chart: Ctor,
993
+ id: 'restaurant_status_chart',
994
+ type: 'pie',
995
+ data: {
996
+ labels: statuses.slice(1),
997
+ datasets: [{
998
+ data: statusCounts(),
999
+ backgroundColor: [chartColors.blue.fill, chartColors.green.fill, chartColors.purple.fill, chartColors.red.fill],
1000
+ borderColor: [chartColors.blue.stroke, chartColors.green.stroke, chartColors.purple.stroke, chartColors.red.stroke],
1001
+ borderWidth: 2,
1002
+ hoverOffset: 4,
1003
+ spacing: 2,
1004
+ }],
1005
+ },
1006
+ options: {
1007
+ ...baseChartOptions,
1008
+ plugins: {
1009
+ legend: { position: 'right', labels: { usePointStyle: true, padding: 20, font: { size: 11 } } },
1010
+ title: { display: false },
1011
+ },
1012
+ },
1013
+ })
1014
+
1015
+ charts = [trend.instance, product.instance, source.instance, status.instance].filter((c): c is ChartLike => c !== null)
1016
+ })
1017
+
1018
+ onDestroy(() => {
1019
+ charts.forEach(c => c.destroy())
1020
+ charts = []
1021
+ })
1022
+ </script>
1023
+
1024
+ @endsection