@nexttylabs/echo 0.2.0
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/.changeset/README.md +21 -0
- package/.changeset/config.json +11 -0
- package/.changeset/cozy-ghosts-care.md +5 -0
- package/.changeset/sharp-lines-stand.md +5 -0
- package/.changeset/sour-doodles-eat.md +5 -0
- package/.changeset/tender-moose-shop.md +5 -0
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/release.yml +73 -0
- package/AGENTS.md +92 -0
- package/CHANGELOG.md +13 -0
- package/Dockerfile +57 -0
- package/LICENSE +661 -0
- package/Makefile +77 -0
- package/README.md +198 -0
- package/app/(auth)/login/page.tsx +53 -0
- package/app/(auth)/register/page.tsx +48 -0
- package/app/(auth)/sign-in/page.tsx +22 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
- package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
- package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
- package/app/(dashboard)/admin/feedback/page.tsx +81 -0
- package/app/(dashboard)/admin/layout.tsx +48 -0
- package/app/(dashboard)/analytics/portal/page.tsx +30 -0
- package/app/(dashboard)/dashboard/page.tsx +133 -0
- package/app/(dashboard)/layout.tsx +69 -0
- package/app/(dashboard)/no-access/page.tsx +45 -0
- package/app/(dashboard)/settings/access/page.tsx +56 -0
- package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
- package/app/(dashboard)/settings/appearance/page.tsx +40 -0
- package/app/(dashboard)/settings/branding/page.tsx +62 -0
- package/app/(dashboard)/settings/changelog/page.tsx +51 -0
- package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
- package/app/(dashboard)/settings/feedback/page.tsx +63 -0
- package/app/(dashboard)/settings/integrations/page.tsx +94 -0
- package/app/(dashboard)/settings/layout.tsx +43 -0
- package/app/(dashboard)/settings/modules/page.tsx +54 -0
- package/app/(dashboard)/settings/notifications/page.tsx +48 -0
- package/app/(dashboard)/settings/organization/page.tsx +104 -0
- package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
- package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
- package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
- package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
- package/app/(dashboard)/settings/page.tsx +22 -0
- package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
- package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
- package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
- package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
- package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
- package/app/(dashboard)/settings/profile/page.tsx +48 -0
- package/app/(dashboard)/settings/widgets/page.tsx +63 -0
- package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
- package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
- package/app/(public)/[organizationSlug]/page.tsx +160 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
- package/app/(public)/docs/page.tsx +48 -0
- package/app/(public)/feedback/[id]/not-found.tsx +33 -0
- package/app/(public)/feedback/[id]/page.tsx +102 -0
- package/app/(public)/invite/[token]/page.tsx +121 -0
- package/app/(public)/page.tsx +22 -0
- package/app/(public)/widget/[organizationId]/page.tsx +122 -0
- package/app/api/_utils.ts +29 -0
- package/app/api/admin/backup/route.ts +72 -0
- package/app/api/api-keys/[keyId]/route.ts +92 -0
- package/app/api/api-keys/route.ts +116 -0
- package/app/api/auth/[...all]/route.ts +21 -0
- package/app/api/auth/clear-session/route.ts +43 -0
- package/app/api/auth/register/handler.ts +176 -0
- package/app/api/auth/register/route.ts +26 -0
- package/app/api/docs/route.ts +28 -0
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
- package/app/api/feedback/[id]/comments/route.ts +421 -0
- package/app/api/feedback/[id]/duplicates/route.ts +285 -0
- package/app/api/feedback/[id]/handler.ts +91 -0
- package/app/api/feedback/[id]/processing-status/route.ts +199 -0
- package/app/api/feedback/[id]/reclassify/route.ts +145 -0
- package/app/api/feedback/[id]/route.ts +511 -0
- package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
- package/app/api/feedback/[id]/sync-github/route.ts +52 -0
- package/app/api/feedback/[id]/vote/route.ts +431 -0
- package/app/api/feedback/bulk/route.ts +212 -0
- package/app/api/feedback/handler.ts +138 -0
- package/app/api/feedback/route.ts +298 -0
- package/app/api/feedback/similar/route.ts +100 -0
- package/app/api/health/route.test.ts +64 -0
- package/app/api/health/route.ts +92 -0
- package/app/api/identify/jwt/route.ts +29 -0
- package/app/api/integrations/github/route.ts +196 -0
- package/app/api/internal/domain-lookup/route.ts +67 -0
- package/app/api/invitations/accept/handler.ts +101 -0
- package/app/api/invitations/accept/route.ts +29 -0
- package/app/api/notifications/preferences/route.ts +109 -0
- package/app/api/organizations/[orgId]/handler.ts +123 -0
- package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
- package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
- package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
- package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
- package/app/api/organizations/[orgId]/members/handler.ts +77 -0
- package/app/api/organizations/[orgId]/members/route.ts +29 -0
- package/app/api/organizations/[orgId]/route.ts +30 -0
- package/app/api/organizations/handler.ts +97 -0
- package/app/api/organizations/route.ts +29 -0
- package/app/api/tags/sync/route.ts +88 -0
- package/app/api/upload/handler.ts +79 -0
- package/app/api/upload/route.ts +37 -0
- package/app/api/v1/feedback/[id]/route.ts +276 -0
- package/app/api/v1/feedback/route.ts +250 -0
- package/app/api/v1/spec/route.ts +356 -0
- package/app/api/webhooks/[webhookId]/route.ts +213 -0
- package/app/api/webhooks/github/route.ts +158 -0
- package/app/api/webhooks/route.ts +143 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +139 -0
- package/app/health/route.ts +108 -0
- package/app/layout.tsx +60 -0
- package/bun.lock +2503 -0
- package/components/api/rate-limit-info.tsx +86 -0
- package/components/api-keys/api-key-manager.tsx +262 -0
- package/components/auth/login-form.tsx +207 -0
- package/components/auth/register-form.tsx +230 -0
- package/components/comment/comment-form.tsx +111 -0
- package/components/comment/internal-notes.tsx +219 -0
- package/components/comment/public-comments.tsx +387 -0
- package/components/component-example-client-only.tsx +29 -0
- package/components/component-example.tsx +519 -0
- package/components/dashboard/index.ts +22 -0
- package/components/dashboard/organization-switcher.tsx +96 -0
- package/components/dashboard/quick-actions.tsx +57 -0
- package/components/dashboard/recent-feedback-list.tsx +152 -0
- package/components/dashboard/stats-cards.tsx +88 -0
- package/components/dashboard/status-chart.tsx +106 -0
- package/components/example.tsx +70 -0
- package/components/feedback/attachment-list.tsx +103 -0
- package/components/feedback/auto-classification-badge.tsx +92 -0
- package/components/feedback/classification-override.tsx +64 -0
- package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
- package/components/feedback/duplicate-suggestions.tsx +188 -0
- package/components/feedback/embedded-feedback-form.tsx +439 -0
- package/components/feedback/feedback-actions.tsx +160 -0
- package/components/feedback/feedback-bulk-actions.tsx +184 -0
- package/components/feedback/feedback-detail-view.tsx +321 -0
- package/components/feedback/feedback-detail.tsx +305 -0
- package/components/feedback/feedback-edit-form.tsx +131 -0
- package/components/feedback/feedback-filters.tsx +222 -0
- package/components/feedback/feedback-list-controls.tsx +433 -0
- package/components/feedback/feedback-list-item.tsx +298 -0
- package/components/feedback/feedback-list-skeleton.tsx +49 -0
- package/components/feedback/feedback-list.tsx +523 -0
- package/components/feedback/feedback-sorter.tsx +117 -0
- package/components/feedback/feedback-stats.tsx +124 -0
- package/components/feedback/file-upload.tsx +289 -0
- package/components/feedback/processing-status.tsx +161 -0
- package/components/feedback/status-history.tsx +134 -0
- package/components/feedback/status-selector.tsx +153 -0
- package/components/feedback/submit-on-behalf-form.tsx +403 -0
- package/components/feedback/tag-suggestions.tsx +212 -0
- package/components/feedback/vote-button.tsx +113 -0
- package/components/feedback/vote-list.tsx +108 -0
- package/components/integrations/github-config.tsx +200 -0
- package/components/landing/hero.tsx +150 -0
- package/components/layout/dashboard-layout.tsx +59 -0
- package/components/layout/index.ts +20 -0
- package/components/layout/language-switcher.tsx +129 -0
- package/components/layout/mobile-sidebar.tsx +66 -0
- package/components/layout/sidebar.tsx +279 -0
- package/components/portal/changelog-entry.tsx +132 -0
- package/components/portal/changelog-list.tsx +85 -0
- package/components/portal/contributor-badge.tsx +29 -0
- package/components/portal/contributors-sidebar.tsx +98 -0
- package/components/portal/create-post-dialog.tsx +247 -0
- package/components/portal/feedback-board.tsx +205 -0
- package/components/portal/feedback-post-card.tsx +198 -0
- package/components/portal/help-center.tsx +169 -0
- package/components/portal/leaderboard.tsx +29 -0
- package/components/portal/portal-header.tsx +153 -0
- package/components/portal/portal-layout.tsx +62 -0
- package/components/portal/portal-modules-panel.tsx +118 -0
- package/components/portal/portal-nav.tsx +59 -0
- package/components/portal/portal-overview.tsx +174 -0
- package/components/portal/portal-settings-nav.tsx +62 -0
- package/components/portal/portal-settings-shell.tsx +71 -0
- package/components/portal/portal-shell.tsx +62 -0
- package/components/portal/portal-tab-nav.tsx +77 -0
- package/components/portal/project-switcher.tsx +20 -0
- package/components/portal/roadmap-board.tsx +82 -0
- package/components/portal/roadmap-card.tsx +76 -0
- package/components/portal/roadmap-column.tsx +78 -0
- package/components/portal/settings-forms/access-form.tsx +194 -0
- package/components/portal/settings-forms/copy-form.tsx +95 -0
- package/components/portal/settings-forms/index.ts +23 -0
- package/components/portal/settings-forms/languages-form.tsx +223 -0
- package/components/portal/settings-forms/seo-form.tsx +156 -0
- package/components/portal/settings-forms/sharing-form.tsx +155 -0
- package/components/portal/settings-forms/theme-form.tsx +104 -0
- package/components/settings/api-keys-list.tsx +167 -0
- package/components/settings/appearance-form.tsx +71 -0
- package/components/settings/index.ts +25 -0
- package/components/settings/invite-member-form.tsx +119 -0
- package/components/settings/notification-preferences.tsx +174 -0
- package/components/settings/organization-form.tsx +165 -0
- package/components/settings/organization-members-list.tsx +197 -0
- package/components/settings/profile-form.tsx +124 -0
- package/components/settings/role-selector.tsx +57 -0
- package/components/settings/settings-sidebar.tsx +115 -0
- package/components/shared/pagination.tsx +215 -0
- package/components/ui/alert-dialog.tsx +201 -0
- package/components/ui/alert.tsx +75 -0
- package/components/ui/avatar.tsx +126 -0
- package/components/ui/badge.tsx +62 -0
- package/components/ui/button.tsx +77 -0
- package/components/ui/card.tsx +111 -0
- package/components/ui/combobox.tsx +311 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +272 -0
- package/components/ui/field.tsx +256 -0
- package/components/ui/input-group.tsx +164 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/label.tsx +41 -0
- package/components/ui/pagination.tsx +142 -0
- package/components/ui/select.tsx +202 -0
- package/components/ui/separator.tsx +45 -0
- package/components/ui/sheet.tsx +151 -0
- package/components/ui/skeleton.tsx +32 -0
- package/components/ui/switch.tsx +49 -0
- package/components/ui/table.tsx +118 -0
- package/components/ui/tabs.tsx +107 -0
- package/components/ui/textarea.tsx +35 -0
- package/components/ui/tooltip.tsx +78 -0
- package/components/widget/widget-form.tsx +439 -0
- package/components.json +24 -0
- package/db/init/01-init.sql +13 -0
- package/docker-compose.dev.yml +26 -0
- package/docker-compose.yml +98 -0
- package/docs/architecture.md +259 -0
- package/docs/component-inventory.md +261 -0
- package/docs/database-migrations.md +76 -0
- package/docs/development-guide.md +209 -0
- package/docs/e2e-user-flows.csv +31 -0
- package/docs/er-diagram-feedback.mmd +138 -0
- package/docs/er-diagram.mmd +281 -0
- package/docs/i18n-check-report.md +296 -0
- package/docs/index.md +214 -0
- package/docs/logic-chain.md +94 -0
- package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
- package/docs/plans/2026-01-02-user-login-design.md +37 -0
- package/docs/plans/2026-01-02-user-login.md +437 -0
- package/docs/plans/2026-01-02-user-registration-design.md +47 -0
- package/docs/plans/2026-01-02-user-registration.md +628 -0
- package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
- package/docs/plans/2026-01-03-roles-permissions.md +266 -0
- package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
- package/docs/plans/2026-01-05-member-removal.md +186 -0
- package/docs/plans/2026-01-05-organization-creation.md +374 -0
- package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
- package/docs/plans/2026-01-05-role-configuration.md +441 -0
- package/docs/plans/2026-01-06-file-upload-support.md +804 -0
- package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
- package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
- package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
- package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
- package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
- package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
- package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
- package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
- package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
- package/docs/plans/2026-01-09-settings-center-design.md +114 -0
- package/docs/plans/2026-01-09-settings-center.md +948 -0
- package/docs/plans/2026-01-10-organization-only-design.md +66 -0
- package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
- package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
- package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
- package/docs/plans/2026-01-18-changesets-design.md +40 -0
- package/docs/product_changes.md +37 -0
- package/docs/project-overview.md +159 -0
- package/docs/project-scan-report.json +104 -0
- package/docs/route-role-visibility.md +51 -0
- package/docs/source-tree-analysis.md +150 -0
- package/docs/testing/delete-project-manual-tests.md +18 -0
- package/docs/user-story-tracking.md +191 -0
- package/drizzle.config.ts +32 -0
- package/eslint.config.mjs +19 -0
- package/hooks/use-permissions.ts +56 -0
- package/i18n/config.ts +45 -0
- package/i18n/request.ts +28 -0
- package/i18n/resolve-locale.ts +38 -0
- package/lib/api/errors.ts +62 -0
- package/lib/auth/cli-config.ts +35 -0
- package/lib/auth/client.ts +20 -0
- package/lib/auth/config.ts +55 -0
- package/lib/auth/jwt-identity.ts +21 -0
- package/lib/auth/org-context.ts +71 -0
- package/lib/auth/organization.ts +107 -0
- package/lib/auth/permissions.ts +87 -0
- package/lib/auth/session.ts +23 -0
- package/lib/config/rate-limits.ts +64 -0
- package/lib/dashboard/get-dashboard-stats.ts +136 -0
- package/lib/db/index.ts +41 -0
- package/lib/db/migrate.test.ts +49 -0
- package/lib/db/migrate.ts +62 -0
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
- package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
- package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
- package/lib/db/migrations/0003_add_org_description.sql +1 -0
- package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
- package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
- package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
- package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
- package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
- package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
- package/lib/db/migrations/0010_kind_junta.sql +8 -0
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
- package/lib/db/migrations/0012_giant_power_man.sql +24 -0
- package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
- package/lib/db/migrations/0014_blue_alice.sql +18 -0
- package/lib/db/migrations/0015_webhook_tables.sql +41 -0
- package/lib/db/migrations/0016_github_integration.sql +30 -0
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
- package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
- package/lib/db/migrations/0018_same_spitfire.sql +1 -0
- package/lib/db/migrations/0019_jittery_loners.sql +16 -0
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
- package/lib/db/migrations/meta/0000_snapshot.json +374 -0
- package/lib/db/migrations/meta/0001_snapshot.json +553 -0
- package/lib/db/migrations/meta/0002_snapshot.json +560 -0
- package/lib/db/migrations/meta/0003_snapshot.json +650 -0
- package/lib/db/migrations/meta/0004_snapshot.json +852 -0
- package/lib/db/migrations/meta/0005_snapshot.json +900 -0
- package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
- package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
- package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
- package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
- package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
- package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
- package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
- package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
- package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
- package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
- package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
- package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
- package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
- package/lib/db/migrations/meta/_journal.json +146 -0
- package/lib/db/schema/ai-processing.ts +90 -0
- package/lib/db/schema/api-keys.ts +61 -0
- package/lib/db/schema/attachments.ts +48 -0
- package/lib/db/schema/auth.ts +111 -0
- package/lib/db/schema/comments.ts +74 -0
- package/lib/db/schema/duplicates.ts +80 -0
- package/lib/db/schema/feedback.ts +88 -0
- package/lib/db/schema/github-integrations.ts +66 -0
- package/lib/db/schema/index.ts +35 -0
- package/lib/db/schema/invitations.ts +32 -0
- package/lib/db/schema/notifications.ts +85 -0
- package/lib/db/schema/organization-members.ts +37 -0
- package/lib/db/schema/organization-settings.ts +134 -0
- package/lib/db/schema/organizations.ts +30 -0
- package/lib/db/schema/projects.ts +145 -0
- package/lib/db/schema/status-history.ts +63 -0
- package/lib/db/schema/tags.ts +194 -0
- package/lib/db/schema/user-profiles.ts +31 -0
- package/lib/db/schema/votes.ts +60 -0
- package/lib/db/schema/webhooks.ts +106 -0
- package/lib/feedback/filters.ts +28 -0
- package/lib/feedback/find-similar.ts +49 -0
- package/lib/feedback/get-feedback-by-id.ts +159 -0
- package/lib/feedback/prefill.ts +51 -0
- package/lib/http/get-request-url.ts +28 -0
- package/lib/integrations/github.ts +159 -0
- package/lib/invitations.ts +22 -0
- package/lib/logger.test.ts +31 -0
- package/lib/logger.ts +58 -0
- package/lib/middleware/api-key.ts +126 -0
- package/lib/middleware/rate-limit-keys.ts +47 -0
- package/lib/middleware/rate-limit.ts +148 -0
- package/lib/middleware/rbac.ts +39 -0
- package/lib/middleware/request-id.test.ts +28 -0
- package/lib/middleware/request-id.ts +30 -0
- package/lib/middleware/request-logger.test.ts +36 -0
- package/lib/middleware/request-logger.ts +41 -0
- package/lib/middleware/with-rate-limit.ts +33 -0
- package/lib/portal/analytics.ts +20 -0
- package/lib/portal/contributors.ts +27 -0
- package/lib/portal/i18n.ts +20 -0
- package/lib/portal/leaderboard-settings.ts +20 -0
- package/lib/portal/modules.ts +20 -0
- package/lib/portal/portal-copy.ts +20 -0
- package/lib/portal/public-context.tsx +110 -0
- package/lib/portal/seo.ts +20 -0
- package/lib/portal/settings-context.ts +56 -0
- package/lib/portal/sharing.ts +20 -0
- package/lib/portal/sorting.ts +20 -0
- package/lib/portal/theme.ts +20 -0
- package/lib/services/ai/classifier.ts +296 -0
- package/lib/services/ai/duplicate-detector.ts +255 -0
- package/lib/services/ai/tag-suggester.ts +108 -0
- package/lib/services/api-keys.ts +164 -0
- package/lib/services/backup.ts +173 -0
- package/lib/services/email/templates.ts +158 -0
- package/lib/services/email.ts +68 -0
- package/lib/services/github-sync.ts +205 -0
- package/lib/services/notifications/index.ts +224 -0
- package/lib/services/portal-settings.ts +157 -0
- package/lib/swagger/config.ts +296 -0
- package/lib/swagger/generate.ts +400 -0
- package/lib/upload/file-validator.ts +52 -0
- package/lib/upload/storage.ts +59 -0
- package/lib/utils/format.ts +26 -0
- package/lib/utils/slug.ts +28 -0
- package/lib/utils.ts +23 -0
- package/lib/validations/auth.ts +56 -0
- package/lib/validations/comment.ts +44 -0
- package/lib/validations/feedback.ts +51 -0
- package/lib/validations/invitations.ts +23 -0
- package/lib/validations/organizations.ts +34 -0
- package/lib/validations/projects.ts +49 -0
- package/lib/validators/feedback.ts +57 -0
- package/lib/validators/index.ts +18 -0
- package/lib/webhooks/events.ts +73 -0
- package/lib/webhooks/index.ts +21 -0
- package/lib/webhooks/retry.ts +188 -0
- package/lib/webhooks/sender.ts +183 -0
- package/lib/webhooks/verify.ts +37 -0
- package/lib/workers/feedback-processor.ts +255 -0
- package/messages/en.json +965 -0
- package/messages/jp.json +862 -0
- package/messages/zh-CN.json +855 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +66 -0
- package/package.json +84 -0
- package/playwright.config.ts +44 -0
- package/postcss.config.mjs +7 -0
- package/proxy.test.ts +131 -0
- package/proxy.ts +190 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo-64.svg +5 -0
- package/public/logo.svg +5 -0
- package/public/next.svg +1 -0
- package/public/openapi.json +673 -0
- package/public/uploads/.gitkeep +0 -0
- package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
- package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
- package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
- package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
- package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
- package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
- package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
- package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
- package/public/vercel.svg +1 -0
- package/public/widget/embed.js +249 -0
- package/public/window.svg +1 -0
- package/scripts/backup-db.sh +57 -0
- package/scripts/backup-db.ts +24 -0
- package/scripts/generate-openapi.ts +22 -0
- package/scripts/migration-helper.ts +39 -0
- package/scripts/pre-deploy.ts +75 -0
- package/scripts/restore-db.sh +60 -0
- package/scripts/rollback.ts +72 -0
- package/scripts/seed-tags.ts +48 -0
- package/tests/api/feedback-bulk.test.ts +47 -0
- package/tests/api/feedback-by-id.test.ts +67 -0
- package/tests/api/feedback-comments-route-import.test.ts +26 -0
- package/tests/api/feedback-create.test.ts +71 -0
- package/tests/api/feedback-delete.test.ts +160 -0
- package/tests/api/feedback-filter.test.ts +250 -0
- package/tests/api/feedback-list.test.ts +234 -0
- package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
- package/tests/api/feedback-similar.test.ts +46 -0
- package/tests/api/feedback-sort.test.ts +261 -0
- package/tests/api/feedback-status-enum.test.ts +49 -0
- package/tests/api/feedback-status-filter.test.ts +117 -0
- package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
- package/tests/api/feedback.test.ts +175 -0
- package/tests/api/identify-jwt.test.ts +25 -0
- package/tests/api/invitation-accept.test.ts +213 -0
- package/tests/api/organization-invitations.test.ts +186 -0
- package/tests/api/organization-members-list.test.ts +79 -0
- package/tests/api/organization-members.test.ts +340 -0
- package/tests/api/organizations.test.ts +149 -0
- package/tests/api/register.test.ts +112 -0
- package/tests/api/upload.test.ts +103 -0
- package/tests/api/vote.test.ts +82 -0
- package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
- package/tests/app/admin-feedback-list-page.test.tsx +25 -0
- package/tests/app/admin-feedback-new-page.test.tsx +25 -0
- package/tests/app/health-route-helpers.test.ts +27 -0
- package/tests/app/login-page.test.ts +26 -0
- package/tests/app/portal-page.test.ts +29 -0
- package/tests/app/project-portal-overview.test.tsx +25 -0
- package/tests/app/widget-page-import.test.ts +25 -0
- package/tests/components/create-post-dialog-defaults.test.ts +43 -0
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
- package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
- package/tests/components/feedback/feedback-detail.test.tsx +25 -0
- package/tests/components/feedback/feedback-stats.test.tsx +49 -0
- package/tests/components/feedback-bulk-actions.test.tsx +39 -0
- package/tests/components/feedback-i18n-keys.test.ts +70 -0
- package/tests/components/feedback-list-controls-compile.test.ts +25 -0
- package/tests/components/feedback-list-controls.test.tsx +204 -0
- package/tests/components/feedback-list-item.test.tsx +67 -0
- package/tests/components/landing/hero.test.tsx +46 -0
- package/tests/components/layout/language-switcher.test.tsx +25 -0
- package/tests/components/layout/sidebar.test.tsx +157 -0
- package/tests/components/login-form.test.ts +25 -0
- package/tests/components/organization-form.test.ts +32 -0
- package/tests/components/organization-switcher.test.ts +25 -0
- package/tests/components/pagination.test.tsx +43 -0
- package/tests/components/portal-overview.test.tsx +25 -0
- package/tests/components/profile-form.test.tsx +139 -0
- package/tests/components/role-selector.test.ts +31 -0
- package/tests/components/status-chart.test.tsx +90 -0
- package/tests/e2e/auth.e2e.ts +323 -0
- package/tests/e2e/feedback-actions.e2e.ts +471 -0
- package/tests/e2e/feedback-attachment.e2e.ts +168 -0
- package/tests/e2e/feedback-customer.e2e.ts +226 -0
- package/tests/e2e/feedback-management.e2e.ts +565 -0
- package/tests/e2e/feedback-submit.e2e.ts +133 -0
- package/tests/e2e/feedback-view.e2e.ts +297 -0
- package/tests/e2e/fixtures/test-data.ts +235 -0
- package/tests/e2e/health-check.e2e.ts +230 -0
- package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
- package/tests/e2e/helpers/test-utils.ts +298 -0
- package/tests/e2e/integration-placeholders.e2e.ts +199 -0
- package/tests/e2e/organization.e2e.ts +292 -0
- package/tests/e2e/permissions.e2e.ts +424 -0
- package/tests/e2e/project-widget.e2e.ts +63 -0
- package/tests/feedback/filters.test.ts +29 -0
- package/tests/hooks/use-permissions.test.ts +52 -0
- package/tests/lib/ai/classifier.test.ts +104 -0
- package/tests/lib/ai/duplicate-detector.test.ts +234 -0
- package/tests/lib/attachments-schema.test.ts +30 -0
- package/tests/lib/auth/session.test.ts +49 -0
- package/tests/lib/auth-client.test.ts +37 -0
- package/tests/lib/auth-config.test.ts +26 -0
- package/tests/lib/feedback-prefill.test.ts +52 -0
- package/tests/lib/feedback-processor.test.ts +41 -0
- package/tests/lib/feedback-schema.test.ts +33 -0
- package/tests/lib/file-validator.test.ts +48 -0
- package/tests/lib/get-feedback-by-id.test.ts +37 -0
- package/tests/lib/invitations.test.ts +35 -0
- package/tests/lib/login-schema.test.ts +36 -0
- package/tests/lib/org-context.test.ts +95 -0
- package/tests/lib/organization-access.test.ts +44 -0
- package/tests/lib/organization-member-role-schema.test.ts +41 -0
- package/tests/lib/permissions.test.ts +88 -0
- package/tests/lib/portal-analytics.test.ts +25 -0
- package/tests/lib/portal-contributors.test.ts +25 -0
- package/tests/lib/portal-copy.test.ts +27 -0
- package/tests/lib/portal-i18n.test.ts +30 -0
- package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
- package/tests/lib/portal-modules.test.ts +25 -0
- package/tests/lib/portal-seo.test.ts +25 -0
- package/tests/lib/portal-sharing.test.ts +25 -0
- package/tests/lib/portal-sorting.test.ts +25 -0
- package/tests/lib/portal-theme.test.ts +25 -0
- package/tests/lib/rate-limit.test.ts +142 -0
- package/tests/lib/resolve-locale.test.ts +34 -0
- package/tests/lib/services/backup.test.ts +145 -0
- package/tests/lib/user-organizations.test.ts +42 -0
- package/tests/lib/user-role-schema.test.ts +33 -0
- package/tests/lib/user-schema.test.ts +25 -0
- package/tests/setup.ts +74 -0
- package/tsconfig.json +34 -0
- package/types/bun-test.d.ts +31 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Link from "next/link";
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
24
|
+
import { Badge } from "@/components/ui/badge";
|
|
25
|
+
import { Button } from "@/components/ui/button";
|
|
26
|
+
import { ArrowRight, Inbox } from "lucide-react";
|
|
27
|
+
|
|
28
|
+
interface FeedbackItem {
|
|
29
|
+
feedbackId: number;
|
|
30
|
+
title: string;
|
|
31
|
+
type: string;
|
|
32
|
+
status: string;
|
|
33
|
+
priority: string;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RecentFeedbackListProps {
|
|
38
|
+
feedback: FeedbackItem[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const statusColors: Record<string, string> = {
|
|
42
|
+
new: "bg-blue-100 text-blue-800",
|
|
43
|
+
in_progress: "bg-yellow-100 text-yellow-800",
|
|
44
|
+
resolved: "bg-green-100 text-green-800",
|
|
45
|
+
closed: "bg-gray-100 text-gray-800",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const priorityColors: Record<string, string> = {
|
|
49
|
+
low: "bg-slate-100 text-slate-800",
|
|
50
|
+
medium: "bg-orange-100 text-orange-800",
|
|
51
|
+
high: "bg-red-100 text-red-800",
|
|
52
|
+
critical: "bg-red-200 text-red-900",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function RecentFeedbackList({ feedback }: RecentFeedbackListProps) {
|
|
56
|
+
const t = useTranslations("dashboard.recentFeedback");
|
|
57
|
+
const tFeedback = useTranslations("feedback");
|
|
58
|
+
const tCommon = useTranslations("common");
|
|
59
|
+
|
|
60
|
+
const getStatusLabel = (status: string) => {
|
|
61
|
+
const key = status === "in_progress" ? "inProgress" : status;
|
|
62
|
+
return tFeedback(`status.${key}`) || status;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getTypeLabel = (type: string) => {
|
|
66
|
+
return tFeedback(`type.${type}`) || type;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getPriorityLabel = (priority: string) => {
|
|
70
|
+
return tFeedback(`priority.${priority}`) || priority;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const formatDate = (date: Date): string => {
|
|
74
|
+
const d = new Date(date);
|
|
75
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
76
|
+
const month = months[d.getMonth()];
|
|
77
|
+
const day = d.getDate();
|
|
78
|
+
const hours = d.getHours();
|
|
79
|
+
const minutes = d.getMinutes().toString().padStart(2, "0");
|
|
80
|
+
const ampm = hours >= 12 ? "PM" : "AM";
|
|
81
|
+
const hour12 = hours % 12 || 12;
|
|
82
|
+
return `${month} ${day}, ${hour12}:${minutes} ${ampm}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (feedback.length === 0) {
|
|
86
|
+
return (
|
|
87
|
+
<Card>
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle>{t("title")}</CardTitle>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent>
|
|
92
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
93
|
+
<Inbox className="h-12 w-12 text-muted-foreground mb-4" />
|
|
94
|
+
<p className="text-muted-foreground mb-4">{t("noData")}</p>
|
|
95
|
+
<Button asChild>
|
|
96
|
+
<Link href="/admin/feedback/new">{tCommon("submitFirst")}</Link>
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Card>
|
|
106
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
107
|
+
<CardTitle>{t("title")}</CardTitle>
|
|
108
|
+
<Button variant="ghost" size="sm" asChild>
|
|
109
|
+
<Link href="/admin/feedback">
|
|
110
|
+
{tCommon("viewAll")} <ArrowRight className="ml-1 h-4 w-4" />
|
|
111
|
+
</Link>
|
|
112
|
+
</Button>
|
|
113
|
+
</CardHeader>
|
|
114
|
+
<CardContent>
|
|
115
|
+
<div className="space-y-4">
|
|
116
|
+
{feedback.map((item) => (
|
|
117
|
+
<Link
|
|
118
|
+
key={item.feedbackId}
|
|
119
|
+
href={`/admin/feedback/${item.feedbackId}`}
|
|
120
|
+
className="block"
|
|
121
|
+
>
|
|
122
|
+
<div className="flex items-center justify-between rounded-lg border p-3 hover:bg-muted/50 transition-colors">
|
|
123
|
+
<div className="space-y-1 flex-1 min-w-0">
|
|
124
|
+
<p className="font-medium truncate">{item.title}</p>
|
|
125
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
126
|
+
<span>{getTypeLabel(item.type)}</span>
|
|
127
|
+
<span>·</span>
|
|
128
|
+
<span>{formatDate(item.createdAt)}</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex items-center gap-2 ml-4">
|
|
132
|
+
<Badge
|
|
133
|
+
variant="secondary"
|
|
134
|
+
className={priorityColors[item.priority] || ""}
|
|
135
|
+
>
|
|
136
|
+
{getPriorityLabel(item.priority)}
|
|
137
|
+
</Badge>
|
|
138
|
+
<Badge
|
|
139
|
+
variant="secondary"
|
|
140
|
+
className={statusColors[item.status] || ""}
|
|
141
|
+
>
|
|
142
|
+
{getStatusLabel(item.status)}
|
|
143
|
+
</Badge>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</Link>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</CardContent>
|
|
150
|
+
</Card>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useTranslations } from "next-intl";
|
|
22
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
23
|
+
import { MessageSquare, Clock, TrendingUp, CheckCircle } from "lucide-react";
|
|
24
|
+
|
|
25
|
+
interface StatsCardsProps {
|
|
26
|
+
totalFeedback: number;
|
|
27
|
+
pendingFeedback: number;
|
|
28
|
+
weeklyFeedback: number;
|
|
29
|
+
resolvedFeedback: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function StatsCards({
|
|
33
|
+
totalFeedback,
|
|
34
|
+
pendingFeedback,
|
|
35
|
+
weeklyFeedback,
|
|
36
|
+
resolvedFeedback,
|
|
37
|
+
}: StatsCardsProps) {
|
|
38
|
+
const t = useTranslations("dashboard.stats");
|
|
39
|
+
|
|
40
|
+
const stats = [
|
|
41
|
+
{
|
|
42
|
+
title: t("totalFeedback"),
|
|
43
|
+
value: totalFeedback,
|
|
44
|
+
icon: MessageSquare,
|
|
45
|
+
color: "text-blue-600",
|
|
46
|
+
bgColor: "bg-blue-100",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: t("pending"),
|
|
50
|
+
value: pendingFeedback,
|
|
51
|
+
icon: Clock,
|
|
52
|
+
color: "text-orange-600",
|
|
53
|
+
bgColor: "bg-orange-100",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: t("weeklyNew"),
|
|
57
|
+
value: weeklyFeedback,
|
|
58
|
+
icon: TrendingUp,
|
|
59
|
+
color: "text-green-600",
|
|
60
|
+
bgColor: "bg-green-100",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: t("resolved"),
|
|
64
|
+
value: resolvedFeedback,
|
|
65
|
+
icon: CheckCircle,
|
|
66
|
+
color: "text-purple-600",
|
|
67
|
+
bgColor: "bg-purple-100",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
73
|
+
{stats.map((stat) => (
|
|
74
|
+
<Card key={stat.title}>
|
|
75
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
76
|
+
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
|
77
|
+
<div className={`rounded-full p-2 ${stat.bgColor}`}>
|
|
78
|
+
<stat.icon className={`h-4 w-4 ${stat.color}`} />
|
|
79
|
+
</div>
|
|
80
|
+
</CardHeader>
|
|
81
|
+
<CardContent>
|
|
82
|
+
<div className="text-2xl font-bold">{stat.value}</div>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useTranslations } from "next-intl";
|
|
22
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
23
|
+
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts";
|
|
24
|
+
|
|
25
|
+
interface StatusChartProps {
|
|
26
|
+
data: { status: string; count: number }[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
30
|
+
new: "#3b82f6",
|
|
31
|
+
"in-progress": "#f59e0b",
|
|
32
|
+
resolved: "#22c55e",
|
|
33
|
+
closed: "#6b7280",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function StatusChart({ data }: StatusChartProps) {
|
|
37
|
+
const t = useTranslations("dashboard.statusChart");
|
|
38
|
+
const tFeedback = useTranslations("feedback");
|
|
39
|
+
|
|
40
|
+
const normalizeStatus = (status: string) =>
|
|
41
|
+
status === "in_progress" ? "in-progress" : status;
|
|
42
|
+
|
|
43
|
+
const getStatusLabel = (status: string) => {
|
|
44
|
+
const normalizedStatus = normalizeStatus(status);
|
|
45
|
+
const key = normalizedStatus === "in-progress" ? "inProgress" : normalizedStatus;
|
|
46
|
+
return tFeedback(`status.${key}`) || status;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const chartData = data.map((item) => ({
|
|
50
|
+
name: getStatusLabel(item.status),
|
|
51
|
+
value: item.count,
|
|
52
|
+
color: STATUS_COLORS[normalizeStatus(item.status)] || "#94a3b8",
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const total = chartData.reduce((sum, item) => sum + item.value, 0);
|
|
56
|
+
|
|
57
|
+
if (total === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<Card>
|
|
60
|
+
<CardHeader>
|
|
61
|
+
<CardTitle>{t("title")}</CardTitle>
|
|
62
|
+
</CardHeader>
|
|
63
|
+
<CardContent>
|
|
64
|
+
<div className="flex items-center justify-center h-[200px] text-muted-foreground">
|
|
65
|
+
{t("noData")}
|
|
66
|
+
</div>
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Card>
|
|
74
|
+
<CardHeader>
|
|
75
|
+
<CardTitle>{t("title")}</CardTitle>
|
|
76
|
+
</CardHeader>
|
|
77
|
+
<CardContent>
|
|
78
|
+
<div className="h-[250px]">
|
|
79
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
80
|
+
<PieChart>
|
|
81
|
+
<Pie
|
|
82
|
+
data={chartData}
|
|
83
|
+
cx="50%"
|
|
84
|
+
cy="50%"
|
|
85
|
+
innerRadius={50}
|
|
86
|
+
outerRadius={80}
|
|
87
|
+
paddingAngle={2}
|
|
88
|
+
dataKey="value"
|
|
89
|
+
>
|
|
90
|
+
{chartData.map((entry, index) => (
|
|
91
|
+
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
92
|
+
))}
|
|
93
|
+
</Pie>
|
|
94
|
+
<Tooltip
|
|
95
|
+
formatter={(value: number | undefined) => [`${value}`, tFeedback("filters.title")]}
|
|
96
|
+
/>
|
|
97
|
+
<Legend
|
|
98
|
+
formatter={(value) => <span className="text-sm">{value}</span>}
|
|
99
|
+
/>
|
|
100
|
+
</PieChart>
|
|
101
|
+
</ResponsiveContainer>
|
|
102
|
+
</div>
|
|
103
|
+
</CardContent>
|
|
104
|
+
</Card>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { cn } from "@/lib/utils"
|
|
19
|
+
|
|
20
|
+
function ExampleWrapper({ className, ...props }: React.ComponentProps<"div">) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="bg-background w-full">
|
|
23
|
+
<div
|
|
24
|
+
data-slot="example-wrapper"
|
|
25
|
+
className={cn(
|
|
26
|
+
"mx-auto grid min-h-screen w-full max-w-5xl min-w-0 content-center items-start gap-8 p-4 pt-2 sm:gap-12 sm:p-6 md:grid-cols-2 md:gap-8 lg:p-12 2xl:max-w-6xl",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Example({
|
|
36
|
+
title,
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
containerClassName,
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<"div"> & {
|
|
42
|
+
title: string
|
|
43
|
+
containerClassName?: string
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
data-slot="example"
|
|
48
|
+
className={cn(
|
|
49
|
+
"mx-auto flex w-full max-w-lg min-w-0 flex-col gap-1 self-stretch lg:max-w-none",
|
|
50
|
+
containerClassName
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
<div className="text-muted-foreground px-1.5 py-2 text-xs font-medium">
|
|
55
|
+
{title}
|
|
56
|
+
</div>
|
|
57
|
+
<div
|
|
58
|
+
data-slot="example-content"
|
|
59
|
+
className={cn(
|
|
60
|
+
"bg-background text-foreground flex min-w-0 flex-1 flex-col items-start gap-6 border border-dashed p-4 sm:p-6 *:[div:not([class*='w-'])]:w-full",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { ExampleWrapper, Example }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Button } from "@/components/ui/button";
|
|
22
|
+
import { Card } from "@/components/ui/card";
|
|
23
|
+
import { File, Download, Image as ImageIcon, FileText, FileSpreadsheet } from "lucide-react";
|
|
24
|
+
import { formatFileSize } from "@/lib/utils/format";
|
|
25
|
+
|
|
26
|
+
interface Attachment {
|
|
27
|
+
attachmentId: number;
|
|
28
|
+
fileName: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
fileSize: number;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AttachmentListProps {
|
|
36
|
+
attachments: Attachment[];
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function AttachmentList({ attachments, className }: AttachmentListProps) {
|
|
41
|
+
if (attachments.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="text-center text-muted-foreground py-4">
|
|
44
|
+
暂无附件
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getFileIcon = (mimeType: string) => {
|
|
50
|
+
if (mimeType.startsWith("image/")) {
|
|
51
|
+
return <ImageIcon className="w-5 h-5 text-blue-500" />;
|
|
52
|
+
}
|
|
53
|
+
if (mimeType.includes("pdf")) {
|
|
54
|
+
return <FileText className="w-5 h-5 text-red-500" />;
|
|
55
|
+
}
|
|
56
|
+
if (mimeType.includes("word")) {
|
|
57
|
+
return <FileText className="w-5 h-5 text-blue-600" />;
|
|
58
|
+
}
|
|
59
|
+
if (mimeType.includes("excel") || mimeType.includes("spreadsheet")) {
|
|
60
|
+
return <FileSpreadsheet className="w-5 h-5 text-green-600" />;
|
|
61
|
+
}
|
|
62
|
+
return <File className="w-5 h-5 text-gray-500" />;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={className}>
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
{attachments.map((attachment) => (
|
|
69
|
+
<Card key={attachment.attachmentId} className="p-3">
|
|
70
|
+
<div className="flex items-center justify-between">
|
|
71
|
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
72
|
+
<span className="shrink-0">
|
|
73
|
+
{getFileIcon(attachment.mimeType)}
|
|
74
|
+
</span>
|
|
75
|
+
<div className="flex-1 min-w-0">
|
|
76
|
+
<p className="font-medium truncate">{attachment.fileName}</p>
|
|
77
|
+
<p className="text-sm text-muted-foreground">
|
|
78
|
+
{formatFileSize(attachment.fileSize)}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<Button
|
|
84
|
+
variant="outline"
|
|
85
|
+
size="sm"
|
|
86
|
+
asChild
|
|
87
|
+
>
|
|
88
|
+
<a
|
|
89
|
+
href={`/${attachment.filePath}`}
|
|
90
|
+
download={attachment.fileName}
|
|
91
|
+
className="flex items-center gap-2"
|
|
92
|
+
>
|
|
93
|
+
<Download className="w-4 h-4" />
|
|
94
|
+
下载
|
|
95
|
+
</a>
|
|
96
|
+
</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</Card>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Badge } from "@/components/ui/badge";
|
|
22
|
+
import {
|
|
23
|
+
Tooltip,
|
|
24
|
+
TooltipContent,
|
|
25
|
+
TooltipProvider,
|
|
26
|
+
TooltipTrigger,
|
|
27
|
+
} from "@/components/ui/tooltip";
|
|
28
|
+
import { Bot } from "lucide-react";
|
|
29
|
+
import { useTranslations } from "next-intl";
|
|
30
|
+
|
|
31
|
+
interface AutoClassificationBadgeProps {
|
|
32
|
+
classification?: {
|
|
33
|
+
type: string;
|
|
34
|
+
priority: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
reasons: string[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function AutoClassificationBadge({
|
|
41
|
+
classification,
|
|
42
|
+
}: AutoClassificationBadgeProps) {
|
|
43
|
+
const t = useTranslations("portal.ai");
|
|
44
|
+
if (!classification) return null;
|
|
45
|
+
|
|
46
|
+
const confidenceColor =
|
|
47
|
+
classification.confidence > 0.7
|
|
48
|
+
? "text-green-600"
|
|
49
|
+
: classification.confidence > 0.4
|
|
50
|
+
? "text-yellow-600"
|
|
51
|
+
: "text-gray-600";
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<TooltipProvider>
|
|
55
|
+
<Tooltip>
|
|
56
|
+
<TooltipTrigger asChild>
|
|
57
|
+
<Badge variant="outline" className="cursor-help gap-1">
|
|
58
|
+
<Bot className="h-3 w-3" />
|
|
59
|
+
Auto-classified
|
|
60
|
+
<span className={confidenceColor}>
|
|
61
|
+
({Math.round(classification.confidence * 100)}%)
|
|
62
|
+
</span>
|
|
63
|
+
</Badge>
|
|
64
|
+
</TooltipTrigger>
|
|
65
|
+
<TooltipContent side="bottom" className="max-w-xs">
|
|
66
|
+
<div className="space-y-2">
|
|
67
|
+
<p className="font-medium">{t("classification")}</p>
|
|
68
|
+
<div className="space-y-1 text-sm">
|
|
69
|
+
<p>
|
|
70
|
+
Type: <span className="capitalize">{classification.type}</span>
|
|
71
|
+
</p>
|
|
72
|
+
<p>
|
|
73
|
+
Priority:{" "}
|
|
74
|
+
<span className="capitalize">{classification.priority}</span>
|
|
75
|
+
</p>
|
|
76
|
+
{classification.reasons.length > 0 && (
|
|
77
|
+
<div className="border-t pt-2">
|
|
78
|
+
<p className="text-muted-foreground mb-1 text-xs">{t("reasons")}</p>
|
|
79
|
+
<ul className="space-y-1 text-xs">
|
|
80
|
+
{classification.reasons.map((reason, i) => (
|
|
81
|
+
<li key={i}>• {reason}</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</TooltipContent>
|
|
89
|
+
</Tooltip>
|
|
90
|
+
</TooltipProvider>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { RefreshCw } from "lucide-react";
|
|
24
|
+
|
|
25
|
+
interface ClassificationOverrideProps {
|
|
26
|
+
feedbackId: number;
|
|
27
|
+
onUpdated: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ClassificationOverride({
|
|
31
|
+
feedbackId,
|
|
32
|
+
onUpdated,
|
|
33
|
+
}: ClassificationOverrideProps) {
|
|
34
|
+
const [isReclassifying, setIsReclassifying] = useState(false);
|
|
35
|
+
|
|
36
|
+
async function reclassify() {
|
|
37
|
+
setIsReclassifying(true);
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`/api/feedback/${feedbackId}/reclassify`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
onUpdated();
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
setIsReclassifying(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Button
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="sm"
|
|
55
|
+
onClick={reclassify}
|
|
56
|
+
disabled={isReclassifying}
|
|
57
|
+
>
|
|
58
|
+
<RefreshCw
|
|
59
|
+
className={`mr-2 h-4 w-4 ${isReclassifying ? "animate-spin" : ""}`}
|
|
60
|
+
/>
|
|
61
|
+
Re-run Classification
|
|
62
|
+
</Button>
|
|
63
|
+
);
|
|
64
|
+
}
|