@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.
- package/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- 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
|