@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,942 @@
1
+ @extends('layouts/default')
2
+
3
+ @section('content')
4
+ @title('Dashboard - Marketing Waitlist')
5
+ <PageLayout>
6
+ <div class="sm:flex sm:items-center sm:justify-between">
7
+ <PageHeader title="Waitlist" description="Manage your product waitlist and notify customers when products become available" />
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">Total Waitlist</dt>
23
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ totalEntries() }}</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() }} 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">Notified</dt>
32
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ notifiedEntries() }}</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-notification-square"></div>
35
+ <span>Customers notified</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">Purchased</dt>
41
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ purchasedEntries() }}</dd>
42
+ <dd class="flex items-center mt-2 text-purple-600 text-sm dark:text-purple-400">
43
+ <div class="mr-1 h-4 w-4 i-hugeicons-shopping-cart-02"></div>
44
+ <span>Completed purchases</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">Conversion Rate</dt>
50
+ <dd class="mt-1 font-semibold text-3xl text-gray-900 tracking-tight dark:text-white">{{ conversionRate() }}%</dd>
51
+ <dd class="flex items-center mt-2 text-sm text-yellow-600 dark:text-yellow-400">
52
+ <div class="mr-1 h-4 w-4 i-hugeicons-analytics-up"></div>
53
+ <span>Purchase rate</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">Waitlist Entries</h3>
62
+ <div class="mt-2 h-80">
63
+ <canvas id="waitlist_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">Order Quantities</h3>
71
+ <div class="mt-2 h-80">
72
+ <canvas id="waitlist_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">Source Distribution</h3>
80
+ <div class="mt-2 h-80">
81
+ <canvas id="waitlist_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="waitlist_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="Notified">Notified</option>
117
+ <option value="Purchased">Purchased</option>
118
+ <option value="Cancelled">Cancelled</option>
119
+ </select>
120
+
121
+ <select x-model="sourceFilter" 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 Sources</option>
123
+ <option value="Website">Website</option>
124
+ <option value="Mobile App">Mobile App</option>
125
+ <option value="In-Store">In-Store</option>
126
+ <option value="Social Media">Social Media</option>
127
+ <option value="Email Campaign">Email Campaign</option>
128
+ <option value="Referral">Referral</option>
129
+ </select>
130
+ </div>
131
+ </div>
132
+
133
+ <div class="flex items-center mt-4 text-gray-500 text-sm dark:text-neutral-400">
134
+ <span class="mr-2">Sort by:</span>
135
+ <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' : '' }}">
136
+ Name
137
+ <span :show="sortBy() === 'name'" class="ml-1">
138
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
139
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
140
+ </span>
141
+ </button>
142
+ <button @click="toggleSort('dateAdded')" class="flex items-center mr-3 hover:text-gray-700 dark:hover:text-gray-300 {{ sortBy() === 'dateAdded' ? 'font-semibold text-blue-600 dark:text-blue-400' : '' }}">
143
+ Date Added
144
+ <span :show="sortBy() === 'dateAdded'" class="ml-1">
145
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
146
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
147
+ </span>
148
+ </button>
149
+ <button @click="toggleSort('quantity')" class="flex items-center hover:text-gray-700 dark:hover:text-gray-300 {{ sortBy() === 'quantity' ? 'font-semibold text-blue-600 dark:text-blue-400' : '' }}">
150
+ Quantity
151
+ <span :show="sortBy() === 'quantity'" class="ml-1">
152
+ <div :show="sortOrder() === 'asc'" class="h-4 w-4 i-hugeicons-arrow-up-01"></div>
153
+ <div :show="sortOrder() === 'desc'" class="h-4 w-4 i-hugeicons-arrow-down-02"></div>
154
+ </span>
155
+ </button>
156
+ </div>
157
+
158
+ <div :show="hasSelection" class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-md">
159
+ <div class="flex items-center justify-between">
160
+ <div class="flex items-center">
161
+ <span class="font-medium text-blue-700 text-sm dark:text-blue-300">{{ selectedCount() }} parties selected</span>
162
+ </div>
163
+ <div class="flex space-x-3">
164
+ <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">
165
+ <div class="mr-1 h-4 w-4 i-hugeicons-notification-square"></div>
166
+ Send Updates
167
+ </button>
168
+ <button @click="bulkChangeStatus('Notified')" 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">
169
+ <div class="mr-1 h-4 w-4 i-hugeicons-checkmark-circle-02"></div>
170
+ Mark as Notified
171
+ </button>
172
+ <button @click="bulkChangeStatus('Purchased')" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-purple-600 hover:bg-purple-500 rounded-md shadow-sm">
173
+ <div class="mr-1 h-4 w-4 i-hugeicons-shopping-cart-02"></div>
174
+ Mark as Purchased
175
+ </button>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="overflow-hidden mt-6 ring-1 ring-black ring-opacity-5 sm:rounded-lg shadow">
181
+ <table class="min-w-full divide-gray-300 divide-y dark:divide-neutral-700">
182
+ <thead class="bg-gray-50 dark:bg-neutral-900">
183
+ <tr>
184
+ <th scope="col" class="relative pl-4 pr-3 py-3.5 sm:pl-6">
185
+ <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" />
186
+ </th>
187
+ <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">Customer Details</th>
188
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Quantity</th>
189
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Status</th>
190
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Date Added</th>
191
+ <th scope="col" class="px-3 py-3.5 font-semibold text-gray-900 text-left text-sm dark:text-white">Source</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="7" class="py-10 text-gray-500 dark:text-neutral-400">No entries in 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.quantity }} {{ entry.quantity === 1 ? 'unit' : 'units' }})</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.quantity }}</div>
214
+ </td>
215
+ <td class="px-3 py-4 text-sm whitespace-nowrap">
216
+ <span class="inline-flex items-center px-2.5 py-0.5 font-medium text-xs rounded-full {{ getStatusClass(entry.status) }}">{{ entry.status }}</span>
217
+ </td>
218
+ <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(entry.dateAdded) }}</td>
219
+ <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ entry.source }}</td>
220
+ <td class="relative pl-3 pr-4 py-4 sm:pr-6 font-medium text-right text-sm whitespace-nowrap">
221
+ <div class="flex justify-end space-x-2">
222
+ <button @click="openEditEntryModal({{ entry.id }})" class="text-gray-400 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 transition-colors">
223
+ <div class="h-5 w-5 i-hugeicons-edit-01"></div>
224
+ <span class="sr-only">Edit</span>
225
+ </button>
226
+ <button :show="{{ entry.status === 'Waiting' ? 'true' : 'false' }}" @click="markAsNotified({{ entry.id }})" class="text-gray-400 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 transition-colors">
227
+ <div class="h-5 w-5 i-hugeicons-notification-square"></div>
228
+ <span class="sr-only">Notify</span>
229
+ </button>
230
+ <button :show="{{ entry.status === 'Notified' ? 'true' : 'false' }}" @click="markAsPurchased({{ entry.id }})" class="text-gray-400 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 transition-colors">
231
+ <div class="h-5 w-5 i-hugeicons-shopping-cart-02"></div>
232
+ <span class="sr-only">Purchase</span>
233
+ </button>
234
+ </div>
235
+ </td>
236
+ </tr>
237
+ </template>
238
+ </tbody>
239
+ </table>
240
+ </div>
241
+
242
+ <div class="flex items-center justify-between mt-5">
243
+ <div class="flex sm:hidden flex-1 justify-between">
244
+ <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>
245
+ <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>
246
+ </div>
247
+ <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
248
+ <div>
249
+ <p class="text-gray-700 text-sm dark:text-neutral-300">
250
+ Showing <span class="font-medium">{{ rangeStart() }}</span> to <span class="font-medium">{{ rangeEnd() }}</span> of <span class="font-medium">{{ totalFiltered() }}</span> results
251
+ </p>
252
+ </div>
253
+ <div>
254
+ <nav class="inline-flex -space-x-px rounded-md shadow-sm isolate" aria-label="Pagination">
255
+ <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">
256
+ <span class="sr-only">Previous</span>
257
+ <div class="h-5 w-5 i-hugeicons-arrow-left-01"></div>
258
+ </button>
259
+ <template :for="page in pageNumbers">
260
+ <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>
261
+ </template>
262
+ <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">
263
+ <span class="sr-only">Next</span>
264
+ <div class="h-5 w-5 i-hugeicons-arrow-right-01"></div>
265
+ </button>
266
+ </nav>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Add/Edit Entry Modal -->
274
+ <div :show="showEntryModal" class="fixed inset-0 overflow-y-auto z-10">
275
+ <div class="flex items-end justify-center sm:items-center p-4 sm:p-0 min-h-full text-center">
276
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="closeEntryModal()"></div>
277
+ <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">
278
+ <div>
279
+ <div class="mt-3 sm:mt-5 text-center">
280
+ <h3 class="font-semibold leading-6 text-gray-900 text-lg dark:text-white">{{ entryModalTitle() }}</h3>
281
+ <div class="mt-2">
282
+ <p class="text-gray-500 text-sm dark:text-neutral-400">{{ entryModalDescription() }}</p>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <div class="mt-5 sm:mt-6">
288
+ <form @submit.prevent="saveEntry()">
289
+ <div class="grid gap-x-4 gap-y-6 grid-cols-1 sm:grid-cols-6">
290
+ <div class="sm:col-span-3">
291
+ <label for="name" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Name</label>
292
+ <div class="mt-1">
293
+ <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" />
294
+ </div>
295
+ </div>
296
+
297
+ <div class="sm:col-span-3">
298
+ <label for="email" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Email</label>
299
+ <div class="mt-1">
300
+ <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" />
301
+ </div>
302
+ </div>
303
+
304
+ <div class="sm:col-span-3">
305
+ <label for="phone" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Phone (optional)</label>
306
+ <div class="mt-1">
307
+ <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" />
308
+ </div>
309
+ </div>
310
+
311
+ <div class="sm:col-span-3">
312
+ <label for="quantity" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Party Size</label>
313
+ <div class="mt-1">
314
+ <input id="quantity" x-model.number="entryQuantity" 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" />
315
+ </div>
316
+ </div>
317
+
318
+ <div class="sm:col-span-3">
319
+ <label for="notificationPreference" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Notification Preference</label>
320
+ <div class="mt-1">
321
+ <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">
322
+ <option value="sms">sms</option>
323
+ <option value="email">email</option>
324
+ <option value="both">both</option>
325
+ </select>
326
+ </div>
327
+ </div>
328
+
329
+ <div class="sm:col-span-3">
330
+ <label for="source" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Source</label>
331
+ <div class="mt-1">
332
+ <input id="source" x-model="entrySource" 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" />
333
+ </div>
334
+ </div>
335
+
336
+ <div class="sm:col-span-6">
337
+ <label for="notes" class="block font-medium text-gray-700 text-sm dark:text-neutral-300">Notes (optional)</label>
338
+ <div class="mt-1">
339
+ <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>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="sm:grid sm:gap-3 sm:grid-cols-2 sm:grid-flow-row-dense mt-5 sm:mt-6">
345
+ <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>
346
+ <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>
347
+ </div>
348
+ </form>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <!-- Notification Modal -->
355
+ <div :show="showNotificationModal" class="fixed inset-0 overflow-y-auto z-10">
356
+ <div class="flex items-end justify-center sm:items-center p-4 sm:p-0 min-h-full text-center">
357
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="closeNotificationModal()"></div>
358
+ <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">
359
+ <div>
360
+ <div class="mt-3 sm:mt-5 text-center">
361
+ <h3 class="font-semibold leading-6 text-gray-900 text-lg dark:text-white">Send Notification</h3>
362
+ <div class="mt-2">
363
+ <p class="text-gray-500 text-sm dark:text-neutral-400">Subject:</p>
364
+ <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" />
365
+ <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Message:</p>
366
+ <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>
367
+ </div>
368
+ </div>
369
+ </div>
370
+
371
+ <div class="mt-5 sm:mt-6">
372
+ <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>
373
+ </div>
374
+ </div>
375
+ </PageLayout>
376
+ <script client>
377
+
378
+ interface WaitlistEntry {
379
+ id: number
380
+ name: string
381
+ email: string
382
+ phone: string | null
383
+ productId: number
384
+ quantity: number
385
+ notificationPreference: 'sms' | 'email' | 'both'
386
+ notes: string | null
387
+ status: string
388
+ dateAdded: string
389
+ notified: boolean
390
+ priority: number
391
+ lastNotified: string | null
392
+ source: string
393
+ }
394
+
395
+ interface ChartLike { destroy: () => void, update: () => void, data: { labels: string[], datasets: Array<{ data: number[] }> } }
396
+ type ChartCtor = new (canvas: HTMLElement, config: { type: string, data: unknown, options?: unknown }) => ChartLike
397
+
398
+ const sources = ['all', 'Website', 'Mobile App', 'In-Store', 'Social Media', 'Email Campaign', 'Referral']
399
+ const statuses = ['all', 'Waiting', 'Notified', 'Purchased', 'Cancelled']
400
+
401
+ const waitlistEntries = state<WaitlistEntry[]>([
402
+ { id: 1, name: 'John Smith', email: 'john.smith@example.com', phone: '+1 (555) 123-4567', productId: 1, quantity: 4, notificationPreference: 'sms', notes: 'Interested in bulk purchase', status: 'Waiting', dateAdded: '2024-01-10', notified: false, priority: 2, lastNotified: null, source: 'Website' },
403
+ { id: 2, name: 'Emily Johnson', email: 'emily.j@example.com', phone: '+1 (555) 987-6543', productId: 2, quantity: 2, notificationPreference: 'email', notes: null, status: 'Waiting', dateAdded: '2024-01-12', notified: false, priority: 1, lastNotified: null, source: 'Website' },
404
+ { id: 3, name: 'Michael Brown', email: 'michael.b@example.com', phone: null, productId: 3, quantity: 3, notificationPreference: 'both', notes: 'VIP customer, notify immediately when available', status: 'Purchased', dateAdded: '2024-01-15', notified: true, priority: 3, lastNotified: '2024-01-15T12:30:00', source: 'Mobile App' },
405
+ { id: 4, name: 'Sarah Wilson', email: 'sarah.w@example.com', phone: '+1 (555) 456-7890', productId: 4, quantity: 2, notificationPreference: 'sms', notes: 'Wants to be notified when back in stock', status: 'Waiting', dateAdded: '2024-01-18', notified: false, priority: 2, lastNotified: null, source: 'Website' },
406
+ { id: 5, name: 'David Lee', email: 'david.l@example.com', phone: '+1 (555) 234-5678', productId: 5, quantity: 3, notificationPreference: 'email', notes: 'Requested notification via SMS', status: 'Purchased', dateAdded: '2024-01-20', notified: true, priority: 2, lastNotified: '2024-01-20T12:30:00', source: 'Mobile App' },
407
+ { id: 6, name: 'Jennifer Martinez', email: 'jennifer.m@example.com', phone: null, productId: 6, quantity: 2, notificationPreference: 'both', notes: null, status: 'Waiting', dateAdded: '2024-01-22', notified: false, priority: 1, lastNotified: null, source: 'Website' },
408
+ { id: 7, name: 'Robert Taylor', email: 'robert.t@example.com', phone: '+1 (555) 876-5432', productId: 7, quantity: 4, notificationPreference: 'sms', notes: 'Wants to purchase multiple units when available', status: 'Waiting', dateAdded: '2024-01-25', notified: false, priority: 3, lastNotified: null, source: 'Website' },
409
+ { id: 8, name: 'Lisa Anderson', email: 'lisa.a@example.com', phone: '+1 (555) 345-6789', productId: 8, quantity: 2, notificationPreference: 'email', notes: null, status: 'Purchased', dateAdded: '2024-01-28', notified: true, priority: 2, lastNotified: '2024-01-28T12:30:00', source: 'Website' },
410
+ ])
411
+
412
+ const searchQuery = state('')
413
+ const statusFilter = state('all')
414
+ const partySizeFilter = state('all')
415
+ const sourceFilter = state('all')
416
+ const sortBy = state('dateAdded')
417
+ const sortOrder = state<'asc' | 'desc'>('asc')
418
+
419
+ const itemsPerPage = 10
420
+ const currentPage = state(1)
421
+
422
+ const showEntryModal = state(false)
423
+ const isEditMode = state(false)
424
+ const editingEntryId = state<number | null>(null)
425
+
426
+ const entryName = state('')
427
+ const entryEmail = state('')
428
+ const entryPhone = state('')
429
+ const entryQuantity = state(2)
430
+ const entryNotificationPreference = state<'sms' | 'email' | 'both'>('sms')
431
+ const entrySource = state('Website')
432
+ const entryNotes = state('')
433
+ const entryPriority = state(1)
434
+
435
+ const showNotificationModal = state(false)
436
+ const notificationSubject = state('Product Now Available')
437
+ const notificationMessage = state('Good news! The product you were waiting for is now available for purchase.')
438
+
439
+ const selectedEntries = state<number[]>([])
440
+ const selectAll = state(false)
441
+
442
+ const trendData = state<Array<{ date: string, count: number }>>(generateTrendData())
443
+
444
+ let charts: ChartLike[] = []
445
+
446
+ const totalEntries = derived(() => waitlistEntries().length)
447
+ const waitingEntries = derived(() => waitlistEntries().filter(e => e.status === 'Waiting').length)
448
+ const notifiedEntries = derived(() => waitlistEntries().filter(e => e.status === 'Notified').length)
449
+ const purchasedEntries = derived(() => waitlistEntries().filter(e => e.status === 'Purchased').length)
450
+ const conversionRate = derived(() => {
451
+ const total = totalEntries()
452
+ return total > 0 ? Math.round((purchasedEntries() / total) * 100) : 0
453
+ })
454
+
455
+ const filteredEntries = derived(() => {
456
+ const list = waitlistEntries()
457
+ const q = searchQuery().toLowerCase()
458
+ const sf = statusFilter()
459
+ const pf = partySizeFilter()
460
+ const srcF = sourceFilter()
461
+ return list.filter((entry) => {
462
+ const matchesSearch = q === ''
463
+ || entry.name.toLowerCase().includes(q)
464
+ || (entry.phone !== null && entry.phone.includes(searchQuery()))
465
+ || entry.email.toLowerCase().includes(q)
466
+
467
+ let matchesQuantity = true
468
+ if (pf !== 'all') {
469
+ const [minStr, maxStr] = pf.split('-')
470
+ if (!minStr) {
471
+ matchesQuantity = false
472
+ }
473
+ else {
474
+ const min = Number.parseInt(minStr, 10)
475
+ if (maxStr) {
476
+ const max = Number.parseInt(maxStr, 10)
477
+ matchesQuantity = entry.quantity >= min && entry.quantity <= max
478
+ }
479
+ else {
480
+ matchesQuantity = entry.quantity >= min
481
+ }
482
+ }
483
+ }
484
+
485
+ const matchesStatus = sf === 'all' || entry.status === sf
486
+ const matchesSource = srcF === 'all' || entry.source === srcF
487
+ return matchesSearch && matchesQuantity && matchesStatus && matchesSource
488
+ })
489
+ })
490
+
491
+ const sortedEntries = derived(() => {
492
+ const list = [...filteredEntries()]
493
+ const sb = sortBy()
494
+ const so = sortOrder()
495
+ return list.sort((a, b) => {
496
+ if (sb === 'name')
497
+ return so === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
498
+ if (sb === 'dateAdded')
499
+ return so === 'asc'
500
+ ? new Date(a.dateAdded).getTime() - new Date(b.dateAdded).getTime()
501
+ : new Date(b.dateAdded).getTime() - new Date(a.dateAdded).getTime()
502
+ if (sb === 'quantity')
503
+ return so === 'asc' ? a.quantity - b.quantity : b.quantity - a.quantity
504
+ return 0
505
+ })
506
+ })
507
+
508
+ const paginatedEntries = derived(() => {
509
+ const start = (currentPage() - 1) * itemsPerPage
510
+ return sortedEntries().slice(start, start + itemsPerPage)
511
+ })
512
+
513
+ const totalPages = derived(() => Math.max(1, Math.ceil(sortedEntries().length / itemsPerPage)))
514
+ const pageNumbers = derived(() => Array.from({ length: totalPages() }, (_, i) => i + 1))
515
+ const totalFiltered = derived(() => filteredEntries().length)
516
+ const rangeStart = derived(() => totalFiltered() === 0 ? 0 : (currentPage() - 1) * itemsPerPage + 1)
517
+ const rangeEnd = derived(() => Math.min(currentPage() * itemsPerPage, totalFiltered()))
518
+ const hasNoEntries = derived(() => paginatedEntries().length === 0)
519
+ const hasSelection = derived(() => selectedEntries().length > 0)
520
+ const selectedCount = derived(() => selectedEntries().length)
521
+
522
+ function entryModalTitle(): string { return isEditMode() ? 'Edit Waitlist Entry' : 'Add to Waitlist' }
523
+ function entryModalDescription(): string { return isEditMode() ? 'Update the details for this waitlist entry.' : 'Fill in the details to add a new customer to the waitlist.' }
524
+ function entryModalSubmitLabel(): string { return isEditMode() ? 'Save Changes' : 'Add to Waitlist' }
525
+
526
+ function isSelected(id: number): boolean { return selectedEntries().includes(id) }
527
+
528
+ function getStatusClass(status: string): string {
529
+ switch (status) {
530
+ 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'
531
+ case 'Notified': return 'bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20 dark:bg-green-900/30 dark:text-green-400'
532
+ case 'Purchased': return 'bg-purple-50 text-purple-700 ring-1 ring-inset ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-400'
533
+ 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'
534
+ }
535
+ }
536
+
537
+ function formatDate(date: string): string {
538
+ return new Date(date).toLocaleDateString()
539
+ }
540
+
541
+ function toggleSort(field: string): void {
542
+ if (sortBy() === field)
543
+ sortOrder.set(sortOrder() === 'asc' ? 'desc' : 'asc')
544
+ else {
545
+ sortBy.set(field)
546
+ sortOrder.set('asc')
547
+ }
548
+ }
549
+
550
+ function nextPage(): void {
551
+ if (currentPage() < totalPages())
552
+ currentPage.update(p => p + 1)
553
+ }
554
+
555
+ function prevPage(): void {
556
+ if (currentPage() > 1)
557
+ currentPage.update(p => p - 1)
558
+ }
559
+
560
+ function goToPage(page: number): void {
561
+ if (page >= 1 && page <= totalPages())
562
+ currentPage.set(page)
563
+ }
564
+
565
+ function resetForm(): void {
566
+ entryName.set('')
567
+ entryEmail.set('')
568
+ entryPhone.set('')
569
+ entryQuantity.set(2)
570
+ entryNotificationPreference.set('sms')
571
+ entrySource.set('Website')
572
+ entryNotes.set('')
573
+ entryPriority.set(1)
574
+ }
575
+
576
+ function openAddEntryModal(): void {
577
+ isEditMode.set(false)
578
+ editingEntryId.set(null)
579
+ resetForm()
580
+ showEntryModal.set(true)
581
+ }
582
+
583
+ function openEditEntryModal(id: number): void {
584
+ const target = waitlistEntries().find(e => e.id === id)
585
+ if (!target)
586
+ return
587
+ isEditMode.set(true)
588
+ editingEntryId.set(id)
589
+ entryName.set(target.name)
590
+ entryEmail.set(target.email)
591
+ entryPhone.set(target.phone ?? '')
592
+ entryQuantity.set(target.quantity)
593
+ entryNotificationPreference.set(target.notificationPreference)
594
+ entrySource.set(target.source)
595
+ entryNotes.set(target.notes ?? '')
596
+ entryPriority.set(target.priority)
597
+ showEntryModal.set(true)
598
+ }
599
+
600
+ function closeEntryModal(): void {
601
+ showEntryModal.set(false)
602
+ editingEntryId.set(null)
603
+ }
604
+
605
+ function saveEntry(): void {
606
+ const now = new Date()
607
+ const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
608
+ const editId = editingEntryId()
609
+ const list = waitlistEntries()
610
+ const id = isEditMode() && editId !== null ? editId : Math.max(0, ...list.map(e => e.id)) + 1
611
+ const entry: WaitlistEntry = {
612
+ id,
613
+ name: entryName(),
614
+ email: entryEmail(),
615
+ phone: entryPhone() || null,
616
+ productId: Math.floor(Math.random() * 1000),
617
+ quantity: entryQuantity(),
618
+ notificationPreference: entryNotificationPreference(),
619
+ notes: entryNotes() || null,
620
+ status: 'Waiting',
621
+ dateAdded: formattedDate,
622
+ notified: false,
623
+ priority: entryPriority(),
624
+ lastNotified: null,
625
+ source: entrySource(),
626
+ }
627
+
628
+ if (isEditMode() && editId !== null)
629
+ waitlistEntries.set(list.map(e => e.id === editId ? entry : e))
630
+ else
631
+ waitlistEntries.set([...list, entry])
632
+
633
+ closeEntryModal()
634
+ refreshCharts()
635
+ }
636
+
637
+ function deleteEntry(id: number): void {
638
+ // eslint-disable-next-line no-alert
639
+ if (!confirm('Are you sure you want to delete this waitlist entry?'))
640
+ return
641
+ waitlistEntries.set(waitlistEntries().filter(e => e.id !== id))
642
+ refreshCharts()
643
+ }
644
+
645
+ function markAsNotified(id: number): void {
646
+ waitlistEntries.set(waitlistEntries().map(e => e.id === id ? { ...e, status: 'Notified', notified: true } : e))
647
+ refreshCharts()
648
+ }
649
+
650
+ function markAsPurchased(id: number): void {
651
+ waitlistEntries.set(waitlistEntries().map(e => e.id === id ? { ...e, status: 'Purchased' } : e))
652
+ refreshCharts()
653
+ }
654
+
655
+ function toggleSelectAll(): void {
656
+ const next = !selectAll()
657
+ selectAll.set(next)
658
+ if (next)
659
+ selectedEntries.set(paginatedEntries().map(entry => entry.id))
660
+ else
661
+ selectedEntries.set([])
662
+ }
663
+
664
+ function toggleEntrySelection(id: number): void {
665
+ const list = selectedEntries()
666
+ if (list.includes(id))
667
+ selectedEntries.set(list.filter(x => x !== id))
668
+ else
669
+ selectedEntries.set([...list, id])
670
+
671
+ const visible = paginatedEntries()
672
+ selectAll.set(visible.length > 0 && visible.every(entry => selectedEntries().includes(entry.id)))
673
+ }
674
+
675
+ function openNotificationModal(): void {
676
+ if (selectedEntries().length === 0) {
677
+ // eslint-disable-next-line no-alert
678
+ alert('Please select at least one waitlist entry to notify')
679
+ return
680
+ }
681
+ notificationSubject.set('Product Now Available')
682
+ notificationMessage.set('Good news! The product you were waiting for is now available for purchase.')
683
+ showNotificationModal.set(true)
684
+ }
685
+
686
+ function closeNotificationModal(): void {
687
+ showNotificationModal.set(false)
688
+ }
689
+
690
+ function sendNotifications(): void {
691
+ const ids = new Set(selectedEntries())
692
+ waitlistEntries.set(waitlistEntries().map(e => ids.has(e.id) ? { ...e, notified: true, status: 'Notified' } : e))
693
+ const count = selectedEntries().length
694
+ selectedEntries.set([])
695
+ selectAll.set(false)
696
+ showNotificationModal.set(false)
697
+ // eslint-disable-next-line no-alert
698
+ alert(`Notifications sent to ${count} customers`)
699
+ refreshCharts()
700
+ }
701
+
702
+ function exportWaitlist(): void {
703
+ // eslint-disable-next-line no-alert
704
+ alert('Waitlist data exported successfully')
705
+ }
706
+
707
+ function bulkChangeStatus(status: string): void {
708
+ if (selectedEntries().length === 0) {
709
+ // eslint-disable-next-line no-alert
710
+ alert('Please select at least one waitlist entry')
711
+ return
712
+ }
713
+ const ids = new Set(selectedEntries())
714
+ waitlistEntries.set(waitlistEntries().map((e) => {
715
+ if (!ids.has(e.id))
716
+ return e
717
+ if (status === 'Notified')
718
+ return { ...e, status, notified: true }
719
+ return { ...e, status }
720
+ }))
721
+ selectedEntries.set([])
722
+ selectAll.set(false)
723
+ refreshCharts()
724
+ }
725
+
726
+ function bulkDelete(): void {
727
+ if (selectedEntries().length === 0) {
728
+ // eslint-disable-next-line no-alert
729
+ alert('Please select at least one waitlist entry')
730
+ return
731
+ }
732
+ // eslint-disable-next-line no-alert
733
+ if (!confirm(`Are you sure you want to delete ${selectedEntries().length} waitlist entries?`))
734
+ return
735
+ const ids = new Set(selectedEntries())
736
+ waitlistEntries.set(waitlistEntries().filter(e => !ids.has(e.id)))
737
+ selectedEntries.set([])
738
+ selectAll.set(false)
739
+ refreshCharts()
740
+ }
741
+
742
+ function generateTrendData(): Array<{ date: string, count: number }> {
743
+ const data: Array<{ date: string, count: number }> = []
744
+ const today = new Date()
745
+ for (let i = 29; i >= 0; i--) {
746
+ const d = new Date(today)
747
+ d.setDate(d.getDate() - i)
748
+ data.push({ date: d.toISOString().split('T')[0] ?? '', count: Math.floor(Math.random() * 5) })
749
+ }
750
+ return data
751
+ }
752
+
753
+ const chartColors = {
754
+ blue: { fill: 'rgba(59, 130, 246, 0.2)', stroke: 'rgba(59, 130, 246, 1)', point: 'rgba(59, 130, 246, 1)' },
755
+ green: { fill: 'rgba(16, 185, 129, 0.2)', stroke: 'rgba(16, 185, 129, 1)', point: 'rgba(16, 185, 129, 1)' },
756
+ purple: { fill: 'rgba(139, 92, 246, 0.2)', stroke: 'rgba(139, 92, 246, 1)', point: 'rgba(139, 92, 246, 1)' },
757
+ yellow: { fill: 'rgba(245, 158, 11, 0.2)', stroke: 'rgba(245, 158, 11, 1)', point: 'rgba(245, 158, 11, 1)' },
758
+ red: { fill: 'rgba(239, 68, 68, 0.2)', stroke: 'rgba(239, 68, 68, 1)', point: 'rgba(239, 68, 68, 1)' },
759
+ }
760
+
761
+ const baseChartOptions = {
762
+ responsive: true,
763
+ maintainAspectRatio: false,
764
+ plugins: {
765
+ legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 20, font: { size: 12 } } },
766
+ title: { display: false },
767
+ },
768
+ }
769
+
770
+ const baseChartOptionsWithScales = {
771
+ ...baseChartOptions,
772
+ scales: {
773
+ y: { beginAtZero: true, grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }, ticks: { precision: 0 } },
774
+ x: { grid: { display: false } },
775
+ },
776
+ }
777
+
778
+ function partySizeCounts(): number[] {
779
+ const list = waitlistEntries()
780
+ return [1, 2, 3, 4, 5, 6, 7].map((size) => {
781
+ if (size === 7)
782
+ return list.filter(e => e.quantity >= 7).length
783
+ return list.filter(e => e.quantity === size).length
784
+ })
785
+ }
786
+
787
+ function partySizeLabels(): string[] {
788
+ return [1, 2, 3, 4, 5, 6, '7+'].map(size => `${size} ${size === 1 ? 'unit' : 'units'}`)
789
+ }
790
+
791
+ function sourceCounts(): number[] {
792
+ const list = waitlistEntries()
793
+ return sources.slice(1).map(source => list.filter(e => e.source === source).length)
794
+ }
795
+
796
+ function statusCounts(): number[] {
797
+ const list = waitlistEntries()
798
+ return statuses.slice(1).map(status => list.filter(e => e.status === status).length)
799
+ }
800
+
801
+ function refreshCharts(): void {
802
+ if (charts.length !== 4)
803
+ return
804
+ const [trend, product, source, status] = charts
805
+ if (trend) {
806
+ trend.data.labels = trendData().map(d => d.date)
807
+ if (trend.data.datasets[0])
808
+ trend.data.datasets[0].data = trendData().map(d => d.count)
809
+ trend.update()
810
+ }
811
+ if (product) {
812
+ product.data.labels = partySizeLabels()
813
+ if (product.data.datasets[0])
814
+ product.data.datasets[0].data = partySizeCounts()
815
+ product.update()
816
+ }
817
+ if (source) {
818
+ source.data.labels = sources.slice(1)
819
+ if (source.data.datasets[0])
820
+ source.data.datasets[0].data = sourceCounts()
821
+ source.update()
822
+ }
823
+ if (status) {
824
+ status.data.labels = statuses.slice(1)
825
+ if (status.data.datasets[0])
826
+ status.data.datasets[0].data = statusCounts()
827
+ status.update()
828
+ }
829
+ }
830
+
831
+ onMount(async () => {
832
+ const { Chart, registerables } = await import('@stacksjs/charts')
833
+ Chart.register(...registerables)
834
+ const Ctor = Chart as unknown as ChartCtor
835
+
836
+ const trend = useChart({
837
+ Chart: Ctor,
838
+ id: 'waitlist_trend_chart',
839
+ type: 'line',
840
+ data: {
841
+ labels: trendData().map(d => d.date),
842
+ datasets: [{
843
+ label: 'New Waitlist Entries',
844
+ data: trendData().map(d => d.count),
845
+ backgroundColor: chartColors.blue.fill,
846
+ borderColor: chartColors.blue.stroke,
847
+ borderWidth: 2,
848
+ pointBackgroundColor: chartColors.blue.point,
849
+ tension: 0.4,
850
+ fill: true,
851
+ pointRadius: 4,
852
+ pointHoverRadius: 6,
853
+ }],
854
+ },
855
+ options: {
856
+ ...baseChartOptionsWithScales,
857
+ plugins: { legend: { display: false }, title: { display: false } },
858
+ },
859
+ })
860
+
861
+ const product = useChart({
862
+ Chart: Ctor,
863
+ id: 'waitlist_product_chart',
864
+ type: 'bar',
865
+ data: {
866
+ labels: partySizeLabels(),
867
+ datasets: [{
868
+ label: 'Number of Orders',
869
+ data: partySizeCounts(),
870
+ backgroundColor: chartColors.blue.fill,
871
+ borderColor: chartColors.blue.stroke,
872
+ borderWidth: 2,
873
+ borderRadius: 6,
874
+ maxBarThickness: 60,
875
+ barPercentage: 0.7,
876
+ }],
877
+ },
878
+ options: {
879
+ ...baseChartOptionsWithScales,
880
+ plugins: { legend: { display: false }, title: { display: false } },
881
+ },
882
+ })
883
+
884
+ const source = useChart({
885
+ Chart: Ctor,
886
+ id: 'waitlist_source_chart',
887
+ type: 'doughnut',
888
+ data: {
889
+ labels: sources.slice(1),
890
+ datasets: [{
891
+ data: sourceCounts(),
892
+ backgroundColor: [chartColors.blue.fill, chartColors.green.fill, chartColors.purple.fill, chartColors.yellow.fill, chartColors.red.fill],
893
+ borderColor: [chartColors.blue.stroke, chartColors.green.stroke, chartColors.purple.stroke, chartColors.yellow.stroke, chartColors.red.stroke],
894
+ borderWidth: 2,
895
+ hoverOffset: 4,
896
+ spacing: 2,
897
+ }],
898
+ },
899
+ options: {
900
+ ...baseChartOptions,
901
+ cutout: '75%',
902
+ plugins: {
903
+ legend: { position: 'right', labels: { usePointStyle: true, padding: 20, font: { size: 11 } } },
904
+ title: { display: false },
905
+ },
906
+ },
907
+ })
908
+
909
+ const status = useChart({
910
+ Chart: Ctor,
911
+ id: 'waitlist_status_chart',
912
+ type: 'pie',
913
+ data: {
914
+ labels: statuses.slice(1),
915
+ datasets: [{
916
+ data: statusCounts(),
917
+ backgroundColor: [chartColors.blue.fill, chartColors.green.fill, chartColors.purple.fill, chartColors.red.fill],
918
+ borderColor: [chartColors.blue.stroke, chartColors.green.stroke, chartColors.purple.stroke, chartColors.red.stroke],
919
+ borderWidth: 2,
920
+ hoverOffset: 4,
921
+ spacing: 2,
922
+ }],
923
+ },
924
+ options: {
925
+ ...baseChartOptions,
926
+ plugins: {
927
+ legend: { position: 'right', labels: { usePointStyle: true, padding: 20, font: { size: 11 } } },
928
+ title: { display: false },
929
+ },
930
+ },
931
+ })
932
+
933
+ charts = [trend.instance, product.instance, source.instance, status.instance].filter((c): c is ChartLike => c !== null)
934
+ })
935
+
936
+ onDestroy(() => {
937
+ charts.forEach(c => c.destroy())
938
+ charts = []
939
+ })
940
+ </script>
941
+
942
+ @endsection