Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business): - Package namespace @forte/* → @lunarfront/* - Database forte/forte_test → lunarfront/lunarfront_test - Docker containers, volumes, connection strings - UI branding, localStorage keys, test emails - All documentation and planning docs Generalize music-specific terminology: - instrumentDescription → itemDescription - instrumentCount → itemCount - instrumentType → itemCategory (on service templates) - New migration 0027_generalize_terminology for column renames - Seed data updated with generic examples - RBAC descriptions updated
This commit is contained in:
@@ -1,473 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
||||
import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/users'
|
||||
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
|
||||
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
|
||||
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
|
||||
import { Route as AuthenticatedMembersIndexRouteImport } from './routes/_authenticated/members/index'
|
||||
import { Route as AuthenticatedAccountsIndexRouteImport } from './routes/_authenticated/accounts/index'
|
||||
import { Route as AuthenticatedRolesNewRouteImport } from './routes/_authenticated/roles/new'
|
||||
import { Route as AuthenticatedRolesRoleIdRouteImport } from './routes/_authenticated/roles/$roleId'
|
||||
import { Route as AuthenticatedMembersMemberIdRouteImport } from './routes/_authenticated/members/$memberId'
|
||||
import { Route as AuthenticatedAccountsNewRouteImport } from './routes/_authenticated/accounts/new'
|
||||
import { Route as AuthenticatedAccountsAccountIdRouteImport } from './routes/_authenticated/accounts/$accountId'
|
||||
import { Route as AuthenticatedAccountsAccountIdIndexRouteImport } from './routes/_authenticated/accounts/$accountId/index'
|
||||
import { Route as AuthenticatedAccountsAccountIdTaxExemptionsRouteImport } from './routes/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
import { Route as AuthenticatedAccountsAccountIdProcessorLinksRouteImport } from './routes/_authenticated/accounts/$accountId/processor-links'
|
||||
import { Route as AuthenticatedAccountsAccountIdPaymentMethodsRouteImport } from './routes/_authenticated/accounts/$accountId/payment-methods'
|
||||
import { Route as AuthenticatedAccountsAccountIdMembersRouteImport } from './routes/_authenticated/accounts/$accountId/members'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
||||
id: '/_authenticated',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedUsersRoute = AuthenticatedUsersRouteImport.update({
|
||||
id: '/users',
|
||||
path: '/users',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedProfileRoute = AuthenticatedProfileRouteImport.update({
|
||||
id: '/profile',
|
||||
path: '/profile',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
|
||||
id: '/help',
|
||||
path: '/help',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
|
||||
id: '/roles/',
|
||||
path: '/roles/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersIndexRoute =
|
||||
AuthenticatedMembersIndexRouteImport.update({
|
||||
id: '/members/',
|
||||
path: '/members/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsIndexRoute =
|
||||
AuthenticatedAccountsIndexRouteImport.update({
|
||||
id: '/accounts/',
|
||||
path: '/accounts/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesNewRoute = AuthenticatedRolesNewRouteImport.update({
|
||||
id: '/roles/new',
|
||||
path: '/roles/new',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesRoleIdRoute =
|
||||
AuthenticatedRolesRoleIdRouteImport.update({
|
||||
id: '/roles/$roleId',
|
||||
path: '/roles/$roleId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersMemberIdRoute =
|
||||
AuthenticatedMembersMemberIdRouteImport.update({
|
||||
id: '/members/$memberId',
|
||||
path: '/members/$memberId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsNewRoute =
|
||||
AuthenticatedAccountsNewRouteImport.update({
|
||||
id: '/accounts/new',
|
||||
path: '/accounts/new',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdRoute =
|
||||
AuthenticatedAccountsAccountIdRouteImport.update({
|
||||
id: '/accounts/$accountId',
|
||||
path: '/accounts/$accountId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdIndexRoute =
|
||||
AuthenticatedAccountsAccountIdIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdTaxExemptionsRoute =
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRouteImport.update({
|
||||
id: '/tax-exemptions',
|
||||
path: '/tax-exemptions',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdProcessorLinksRoute =
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRouteImport.update({
|
||||
id: '/processor-links',
|
||||
path: '/processor-links',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdPaymentMethodsRoute =
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRouteImport.update({
|
||||
id: '/payment-methods',
|
||||
path: '/payment-methods',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdMembersRoute =
|
||||
AuthenticatedAccountsAccountIdMembersRouteImport.update({
|
||||
id: '/members',
|
||||
path: '/members',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/help': typeof AuthenticatedHelpRoute
|
||||
'/profile': typeof AuthenticatedProfileRoute
|
||||
'/users': typeof AuthenticatedUsersRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/members/': typeof AuthenticatedMembersIndexRoute
|
||||
'/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/help': typeof AuthenticatedHelpRoute
|
||||
'/profile': typeof AuthenticatedProfileRoute
|
||||
'/users': typeof AuthenticatedUsersRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/accounts': typeof AuthenticatedAccountsIndexRoute
|
||||
'/members': typeof AuthenticatedMembersIndexRoute
|
||||
'/roles': typeof AuthenticatedRolesIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/_authenticated/help': typeof AuthenticatedHelpRoute
|
||||
'/_authenticated/profile': typeof AuthenticatedProfileRoute
|
||||
'/_authenticated/users': typeof AuthenticatedUsersRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/_authenticated/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/_authenticated/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/_authenticated/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/_authenticated/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/_authenticated/members/': typeof AuthenticatedMembersIndexRoute
|
||||
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/_authenticated/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/help'
|
||||
| '/profile'
|
||||
| '/users'
|
||||
| '/accounts/$accountId'
|
||||
| '/accounts/new'
|
||||
| '/members/$memberId'
|
||||
| '/roles/$roleId'
|
||||
| '/roles/new'
|
||||
| '/accounts/'
|
||||
| '/members/'
|
||||
| '/roles/'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/login'
|
||||
| '/help'
|
||||
| '/profile'
|
||||
| '/users'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/members/$memberId'
|
||||
| '/roles/$roleId'
|
||||
| '/roles/new'
|
||||
| '/accounts'
|
||||
| '/members'
|
||||
| '/roles'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_authenticated'
|
||||
| '/login'
|
||||
| '/_authenticated/help'
|
||||
| '/_authenticated/profile'
|
||||
| '/_authenticated/users'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/accounts/$accountId'
|
||||
| '/_authenticated/accounts/new'
|
||||
| '/_authenticated/members/$memberId'
|
||||
| '/_authenticated/roles/$roleId'
|
||||
| '/_authenticated/roles/new'
|
||||
| '/_authenticated/accounts/'
|
||||
| '/_authenticated/members/'
|
||||
| '/_authenticated/roles/'
|
||||
| '/_authenticated/accounts/$accountId/members'
|
||||
| '/_authenticated/accounts/$accountId/payment-methods'
|
||||
| '/_authenticated/accounts/$accountId/processor-links'
|
||||
| '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
| '/_authenticated/accounts/$accountId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
||||
LoginRoute: typeof LoginRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated': {
|
||||
id: '/_authenticated'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated/': {
|
||||
id: '/_authenticated/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/users': {
|
||||
id: '/_authenticated/users'
|
||||
path: '/users'
|
||||
fullPath: '/users'
|
||||
preLoaderRoute: typeof AuthenticatedUsersRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/profile': {
|
||||
id: '/_authenticated/profile'
|
||||
path: '/profile'
|
||||
fullPath: '/profile'
|
||||
preLoaderRoute: typeof AuthenticatedProfileRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/help': {
|
||||
id: '/_authenticated/help'
|
||||
path: '/help'
|
||||
fullPath: '/help'
|
||||
preLoaderRoute: typeof AuthenticatedHelpRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/': {
|
||||
id: '/_authenticated/roles/'
|
||||
path: '/roles'
|
||||
fullPath: '/roles/'
|
||||
preLoaderRoute: typeof AuthenticatedRolesIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members/': {
|
||||
id: '/_authenticated/members/'
|
||||
path: '/members'
|
||||
fullPath: '/members/'
|
||||
preLoaderRoute: typeof AuthenticatedMembersIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/': {
|
||||
id: '/_authenticated/accounts/'
|
||||
path: '/accounts'
|
||||
fullPath: '/accounts/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/new': {
|
||||
id: '/_authenticated/roles/new'
|
||||
path: '/roles/new'
|
||||
fullPath: '/roles/new'
|
||||
preLoaderRoute: typeof AuthenticatedRolesNewRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/$roleId': {
|
||||
id: '/_authenticated/roles/$roleId'
|
||||
path: '/roles/$roleId'
|
||||
fullPath: '/roles/$roleId'
|
||||
preLoaderRoute: typeof AuthenticatedRolesRoleIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members/$memberId': {
|
||||
id: '/_authenticated/members/$memberId'
|
||||
path: '/members/$memberId'
|
||||
fullPath: '/members/$memberId'
|
||||
preLoaderRoute: typeof AuthenticatedMembersMemberIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/new': {
|
||||
id: '/_authenticated/accounts/new'
|
||||
path: '/accounts/new'
|
||||
fullPath: '/accounts/new'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsNewRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId': {
|
||||
id: '/_authenticated/accounts/$accountId'
|
||||
path: '/accounts/$accountId'
|
||||
fullPath: '/accounts/$accountId'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/': {
|
||||
id: '/_authenticated/accounts/$accountId/'
|
||||
path: '/'
|
||||
fullPath: '/accounts/$accountId/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': {
|
||||
id: '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
path: '/tax-exemptions'
|
||||
fullPath: '/accounts/$accountId/tax-exemptions'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/processor-links': {
|
||||
id: '/_authenticated/accounts/$accountId/processor-links'
|
||||
path: '/processor-links'
|
||||
fullPath: '/accounts/$accountId/processor-links'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/payment-methods': {
|
||||
id: '/_authenticated/accounts/$accountId/payment-methods'
|
||||
path: '/payment-methods'
|
||||
fullPath: '/accounts/$accountId/payment-methods'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/members': {
|
||||
id: '/_authenticated/accounts/$accountId/members'
|
||||
path: '/members'
|
||||
fullPath: '/accounts/$accountId/members'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdMembersRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthenticatedAccountsAccountIdRouteChildren {
|
||||
AuthenticatedAccountsAccountIdMembersRoute: typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
AuthenticatedAccountsAccountIdIndexRoute: typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteChildren: AuthenticatedAccountsAccountIdRouteChildren =
|
||||
{
|
||||
AuthenticatedAccountsAccountIdMembersRoute:
|
||||
AuthenticatedAccountsAccountIdMembersRoute,
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute:
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute,
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute:
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute,
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute:
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute,
|
||||
AuthenticatedAccountsAccountIdIndexRoute:
|
||||
AuthenticatedAccountsAccountIdIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteWithChildren =
|
||||
AuthenticatedAccountsAccountIdRoute._addFileChildren(
|
||||
AuthenticatedAccountsAccountIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedHelpRoute: typeof AuthenticatedHelpRoute
|
||||
AuthenticatedProfileRoute: typeof AuthenticatedProfileRoute
|
||||
AuthenticatedUsersRoute: typeof AuthenticatedUsersRoute
|
||||
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
||||
AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
AuthenticatedAccountsNewRoute: typeof AuthenticatedAccountsNewRoute
|
||||
AuthenticatedMembersMemberIdRoute: typeof AuthenticatedMembersMemberIdRoute
|
||||
AuthenticatedRolesRoleIdRoute: typeof AuthenticatedRolesRoleIdRoute
|
||||
AuthenticatedRolesNewRoute: typeof AuthenticatedRolesNewRoute
|
||||
AuthenticatedAccountsIndexRoute: typeof AuthenticatedAccountsIndexRoute
|
||||
AuthenticatedMembersIndexRoute: typeof AuthenticatedMembersIndexRoute
|
||||
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedHelpRoute: AuthenticatedHelpRoute,
|
||||
AuthenticatedProfileRoute: AuthenticatedProfileRoute,
|
||||
AuthenticatedUsersRoute: AuthenticatedUsersRoute,
|
||||
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
||||
AuthenticatedAccountsAccountIdRoute:
|
||||
AuthenticatedAccountsAccountIdRouteWithChildren,
|
||||
AuthenticatedAccountsNewRoute: AuthenticatedAccountsNewRoute,
|
||||
AuthenticatedMembersMemberIdRoute: AuthenticatedMembersMemberIdRoute,
|
||||
AuthenticatedRolesRoleIdRoute: AuthenticatedRolesRoleIdRoute,
|
||||
AuthenticatedRolesNewRoute: AuthenticatedRolesNewRoute,
|
||||
AuthenticatedAccountsIndexRoute: AuthenticatedAccountsIndexRoute,
|
||||
AuthenticatedMembersIndexRoute: AuthenticatedMembersIndexRoute,
|
||||
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
AuthenticatedRouteChildren,
|
||||
)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
||||
LoginRoute: LoginRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Forte Admin</title>
|
||||
<title>LunarFront Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Apply mode before React renders to prevent flash
|
||||
(function() {
|
||||
var mode = localStorage.getItem('forte-mode') || 'system';
|
||||
var mode = localStorage.getItem('lunarfront-mode') || 'system';
|
||||
var isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDark) document.documentElement.classList.add('dark');
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/admin",
|
||||
"name": "@lunarfront/admin",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -10,7 +10,7 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@forte/shared": "workspace:*",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Account } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const accountKeys = {
|
||||
all: ['accounts'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { MemberIdentifier } from '@/types/account'
|
||||
import type { PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const identifierKeys = {
|
||||
all: (memberId: string) => ['members', memberId, 'identifiers'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Member } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
interface MemberWithAccount extends Member {
|
||||
accountName: string | null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaymentMethod } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const paymentMethodKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { ProcessorLink } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const processorLinkKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Permission, Role } from '@/types/rbac'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const rbacKeys = {
|
||||
permissions: ['permissions'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { RepairTicket, RepairLineItem, RepairBatch, RepairNote, RepairServiceTemplate } from '@/types/repair'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Repair Tickets ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { StorageFolder, StorageFolderPermission, StorageFile } from '@/types/storage'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Folders ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { TaxExemption } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const taxExemptionKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export interface UserRole {
|
||||
id: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { VaultStatus, VaultCategory, VaultCategoryPermission, VaultEntry } from '@/types/vault'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Keys ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { AccountCreateSchema } from '@forte/shared/schemas'
|
||||
import { AccountCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PaymentMethodCreateSchema } from '@forte/shared/schemas'
|
||||
import type { PaymentMethodCreateInput } from '@forte/shared/schemas'
|
||||
import { PaymentMethodCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { PaymentMethodCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { ProcessorLinkCreateSchema } from '@forte/shared/schemas'
|
||||
import type { ProcessorLinkCreateInput } from '@forte/shared/schemas'
|
||||
import { ProcessorLinkCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { ProcessorLinkCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { TaxExemptionCreateSchema } from '@forte/shared/schemas'
|
||||
import type { TaxExemptionCreateInput } from '@forte/shared/schemas'
|
||||
import { TaxExemptionCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { TaxExemptionCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -24,7 +24,7 @@ interface GeneratePdfOptions {
|
||||
companyName?: string
|
||||
}
|
||||
|
||||
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'Forte Music' }: GeneratePdfOptions): jsPDF {
|
||||
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'LunarFront' }: GeneratePdfOptions): jsPDF {
|
||||
const doc = new jsPDF()
|
||||
let y = 20
|
||||
|
||||
@@ -57,11 +57,11 @@ export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, comp
|
||||
doc.setFontSize(10)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text('Customer', 14, y)
|
||||
doc.text('Instrument', 110, y)
|
||||
doc.text('Item', 110, y)
|
||||
y += 5
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(ticket.customerName, 14, y)
|
||||
doc.text(ticket.instrumentDescription ?? '-', 110, y)
|
||||
doc.text(ticket.itemDescription ?? '-', 110, y)
|
||||
y += 5
|
||||
if (ticket.customerPhone) { doc.text(ticket.customerPhone, 14, y); y += 5 }
|
||||
if (ticket.serialNumber) { doc.text(`S/N: ${ticket.serialNumber}`, 110, y - 5) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
interface PaginationSearch {
|
||||
page?: number
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/
|
||||
import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings'
|
||||
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
|
||||
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
|
||||
import { Route as AuthenticatedVaultIndexRouteImport } from './routes/_authenticated/vault/index'
|
||||
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
|
||||
import { Route as AuthenticatedRepairsIndexRouteImport } from './routes/_authenticated/repairs/index'
|
||||
import { Route as AuthenticatedRepairBatchesIndexRouteImport } from './routes/_authenticated/repair-batches/index'
|
||||
@@ -72,6 +73,11 @@ const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
|
||||
path: '/help',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedVaultIndexRoute = AuthenticatedVaultIndexRouteImport.update({
|
||||
id: '/vault/',
|
||||
path: '/vault/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
|
||||
id: '/roles/',
|
||||
path: '/roles/',
|
||||
@@ -218,6 +224,7 @@ export interface FileRoutesByFullPath {
|
||||
'/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/repairs/': typeof AuthenticatedRepairsIndexRoute
|
||||
'/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/vault/': typeof AuthenticatedVaultIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -246,6 +253,7 @@ export interface FileRoutesByTo {
|
||||
'/repair-batches': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/repairs': typeof AuthenticatedRepairsIndexRoute
|
||||
'/roles': typeof AuthenticatedRolesIndexRoute
|
||||
'/vault': typeof AuthenticatedVaultIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -277,6 +285,7 @@ export interface FileRoutesById {
|
||||
'/_authenticated/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/_authenticated/repairs/': typeof AuthenticatedRepairsIndexRoute
|
||||
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/_authenticated/vault/': typeof AuthenticatedVaultIndexRoute
|
||||
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -308,6 +317,7 @@ export interface FileRouteTypes {
|
||||
| '/repair-batches/'
|
||||
| '/repairs/'
|
||||
| '/roles/'
|
||||
| '/vault/'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
@@ -336,6 +346,7 @@ export interface FileRouteTypes {
|
||||
| '/repair-batches'
|
||||
| '/repairs'
|
||||
| '/roles'
|
||||
| '/vault'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
@@ -366,6 +377,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticated/repair-batches/'
|
||||
| '/_authenticated/repairs/'
|
||||
| '/_authenticated/roles/'
|
||||
| '/_authenticated/vault/'
|
||||
| '/_authenticated/accounts/$accountId/members'
|
||||
| '/_authenticated/accounts/$accountId/payment-methods'
|
||||
| '/_authenticated/accounts/$accountId/processor-links'
|
||||
@@ -429,6 +441,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedHelpRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/vault/': {
|
||||
id: '/_authenticated/vault/'
|
||||
path: '/vault'
|
||||
fullPath: '/vault/'
|
||||
preLoaderRoute: typeof AuthenticatedVaultIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/': {
|
||||
id: '/_authenticated/roles/'
|
||||
path: '/roles'
|
||||
@@ -628,6 +647,7 @@ interface AuthenticatedRouteChildren {
|
||||
AuthenticatedRepairBatchesIndexRoute: typeof AuthenticatedRepairBatchesIndexRoute
|
||||
AuthenticatedRepairsIndexRoute: typeof AuthenticatedRepairsIndexRoute
|
||||
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
|
||||
AuthenticatedVaultIndexRoute: typeof AuthenticatedVaultIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
@@ -654,6 +674,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedRepairBatchesIndexRoute: AuthenticatedRepairBatchesIndexRoute,
|
||||
AuthenticatedRepairsIndexRoute: AuthenticatedRepairsIndexRoute,
|
||||
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
|
||||
AuthenticatedVaultIndexRoute: AuthenticatedVaultIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
|
||||
@@ -34,7 +34,7 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
|
||||
const ticketColumns: Column<RepairTicket>[] = [
|
||||
{ key: 'ticket_number', header: 'Ticket #', sortable: true, render: (t) => <span className="font-mono text-sm">{t.ticketNumber}</span> },
|
||||
{ key: 'instrument', header: 'Instrument', render: (t) => <>{t.instrumentDescription ?? '-'}</> },
|
||||
{ key: 'item_description', header: 'Item', render: (t) => <>{t.itemDescription ?? '-'}</> },
|
||||
{ key: 'problem', header: 'Problem', render: (t) => <span className="truncate max-w-[200px] block">{t.problemDescription}</span> },
|
||||
{ key: 'status', header: 'Status', sortable: true, render: (t) => <Badge variant="outline">{STATUS_LABELS[t.status] ?? t.status}</Badge> },
|
||||
{
|
||||
@@ -111,7 +111,7 @@ function RepairBatchDetailPage() {
|
||||
|
||||
doc.setFontSize(18)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text('Forte Music', 14, y)
|
||||
doc.text('LunarFront', 14, y)
|
||||
y += 8
|
||||
doc.setFontSize(12)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
@@ -169,7 +169,7 @@ function RepairBatchDetailPage() {
|
||||
doc.setFillColor(245, 245, 245)
|
||||
doc.rect(14, y - 3, 182, 6, 'F')
|
||||
doc.text('Ticket #', 16, y)
|
||||
doc.text('Instrument', 40, y)
|
||||
doc.text('Item', 40, y)
|
||||
doc.text('Problem', 100, y)
|
||||
doc.text('Status', 155, y)
|
||||
doc.text('Estimate', 190, y, { align: 'right' })
|
||||
@@ -179,7 +179,7 @@ function RepairBatchDetailPage() {
|
||||
for (const ticket of tickets) {
|
||||
if (y > 270) { doc.addPage(); y = 20 }
|
||||
doc.text(ticket.ticketNumber ?? '-', 16, y)
|
||||
doc.text((ticket.instrumentDescription ?? '-').slice(0, 30), 40, y)
|
||||
doc.text((ticket.itemDescription ?? '-').slice(0, 30), 40, y)
|
||||
doc.text(ticket.problemDescription.slice(0, 28), 100, y)
|
||||
doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y)
|
||||
doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' })
|
||||
|
||||
@@ -49,9 +49,9 @@ const columns: Column<RepairBatch>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'instruments',
|
||||
header: 'Instruments',
|
||||
render: (b) => <>{b.receivedCount}/{b.instrumentCount}</>,
|
||||
key: 'items',
|
||||
header: 'Items',
|
||||
render: (b) => <>{b.receivedCount}/{b.itemCount}</>,
|
||||
},
|
||||
{
|
||||
key: 'due_date',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { RepairBatchCreateSchema } from '@forte/shared/schemas'
|
||||
import { RepairBatchCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { repairBatchMutations } from '@/api/repairs'
|
||||
import { accountListOptions } from '@/api/accounts'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -167,7 +167,7 @@ function NewRepairBatchPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Notes</Label>
|
||||
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual instrument checkup, multiple guitars needing setups" />
|
||||
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual checkup, multiple items needing service" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -118,7 +118,7 @@ function RepairTicketDetailPage() {
|
||||
setEditFields({
|
||||
customerName: ticket!.customerName,
|
||||
customerPhone: ticket!.customerPhone ?? '',
|
||||
instrumentDescription: ticket!.instrumentDescription ?? '',
|
||||
itemDescription: ticket!.itemDescription ?? '',
|
||||
serialNumber: ticket!.serialNumber ?? '',
|
||||
conditionIn: ticket!.conditionIn ?? '',
|
||||
conditionInNotes: ticket!.conditionInNotes ?? '',
|
||||
@@ -134,7 +134,7 @@ function RepairTicketDetailPage() {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName
|
||||
if (editFields.customerPhone !== (ticket!.customerPhone ?? '')) data.customerPhone = editFields.customerPhone || undefined
|
||||
if (editFields.instrumentDescription !== (ticket!.instrumentDescription ?? '')) data.instrumentDescription = editFields.instrumentDescription || undefined
|
||||
if (editFields.itemDescription !== (ticket!.itemDescription ?? '')) data.itemDescription = editFields.itemDescription || undefined
|
||||
if (editFields.serialNumber !== (ticket!.serialNumber ?? '')) data.serialNumber = editFields.serialNumber || undefined
|
||||
if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined
|
||||
if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined
|
||||
@@ -180,7 +180,7 @@ function RepairTicketDetailPage() {
|
||||
</Button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold">Ticket #{ticket.ticketNumber}</h1>
|
||||
<p className="text-sm text-muted-foreground">{ticket.customerName} — {ticket.instrumentDescription ?? 'No instrument'}</p>
|
||||
<p className="text-sm text-muted-foreground">{ticket.customerName} — {ticket.itemDescription ?? 'No item description'}</p>
|
||||
</div>
|
||||
<PdfModal ticket={ticket} lineItems={lineItemsData?.data ?? []} ticketId={ticketId} />
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ function RepairTicketDetailPage() {
|
||||
<div className="space-y-2"><Label>Phone</Label><Input value={editFields.customerPhone} onChange={(e) => setEditFields((p) => ({ ...p, customerPhone: e.target.value }))} /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>Instrument</Label><Input value={editFields.instrumentDescription} onChange={(e) => setEditFields((p) => ({ ...p, instrumentDescription: e.target.value }))} /></div>
|
||||
<div className="space-y-2"><Label>Item Description</Label><Input value={editFields.itemDescription} onChange={(e) => setEditFields((p) => ({ ...p, itemDescription: e.target.value }))} /></div>
|
||||
<div className="space-y-2"><Label>Serial Number</Label><Input value={editFields.serialNumber} onChange={(e) => setEditFields((p) => ({ ...p, serialNumber: e.target.value }))} /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -310,7 +310,7 @@ function RepairTicketDetailPage() {
|
||||
<div><span className="text-muted-foreground">Account:</span> {ticket.accountId ?? 'Walk-in'}</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><span className="text-muted-foreground">Instrument:</span> {ticket.instrumentDescription ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Item:</span> {ticket.itemDescription ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Serial:</span> {ticket.serialNumber ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Condition:</span> {ticket.conditionIn ?? '-'}</div>
|
||||
</div>
|
||||
@@ -411,8 +411,8 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
|
||||
setDescription(''); setQty('1'); setUnitPrice('0'); setCost(''); setItemType('labor'); setTemplateSearch(''); setShowTemplates(false)
|
||||
}
|
||||
|
||||
function selectTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
|
||||
function selectTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
|
||||
setDescription(desc); setItemType(template.itemType); setUnitPrice(template.defaultPrice); setCost(template.defaultCost ?? ''); setShowTemplates(false); setTemplateSearch('')
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg max-h-48 overflow-auto">
|
||||
{templates.length === 0 ? <div className="p-3 text-sm text-muted-foreground">No templates found</div> : templates.map((t) => (
|
||||
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => selectTemplate(t)}>
|
||||
<span>{t.name}{t.instrumentType ? ` — ${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span>{t.name}{t.itemCategory ? ` — ${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span className="text-muted-foreground">${t.defaultPrice}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import type { RepairTicket } from '@/types/repair'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/repairs/')({
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
@@ -70,9 +70,9 @@ const columns: Column<RepairTicket>[] = [
|
||||
render: (t) => <span className="font-medium">{t.customerName}</span>,
|
||||
},
|
||||
{
|
||||
key: 'instrument',
|
||||
header: 'Instrument',
|
||||
render: (t) => <>{t.instrumentDescription ?? '-'}</>,
|
||||
key: 'item_description',
|
||||
header: 'Item',
|
||||
render: (t) => <>{t.itemDescription ?? '-'}</>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { RepairTicketCreateSchema } from '@forte/shared/schemas'
|
||||
import { RepairTicketCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { repairTicketMutations, repairLineItemMutations, repairServiceTemplateListOptions } from '@/api/repairs'
|
||||
import { accountListOptions } from '@/api/accounts'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
@@ -88,7 +88,7 @@ function NewRepairPage() {
|
||||
defaultValues: {
|
||||
customerName: linkedContactName ?? '',
|
||||
customerPhone: '',
|
||||
instrumentDescription: '',
|
||||
itemDescription: '',
|
||||
serialNumber: '',
|
||||
problemDescription: '',
|
||||
conditionIn: undefined,
|
||||
@@ -157,8 +157,8 @@ function NewRepairPage() {
|
||||
setValue('customerPhone', '')
|
||||
}
|
||||
|
||||
function addFromTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
|
||||
function addFromTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
|
||||
setLineItems((prev) => [...prev, {
|
||||
itemType: template.itemType,
|
||||
description: desc,
|
||||
@@ -297,14 +297,14 @@ function NewRepairPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Instrument Section */}
|
||||
{/* Item Section */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-lg">Instrument</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle className="text-lg">Item</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Instrument Description</Label>
|
||||
<Input {...register('instrumentDescription')} placeholder="e.g. Yamaha Trumpet YTR-2330" />
|
||||
<Label>Item Description</Label>
|
||||
<Input {...register('itemDescription')} placeholder="e.g. Brand, model, description" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Serial Number</Label>
|
||||
@@ -364,7 +364,7 @@ function NewRepairPage() {
|
||||
) : (
|
||||
templates.map((t) => (
|
||||
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => addFromTemplate(t)}>
|
||||
<span>{t.name}{t.instrumentType ? ` — ${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span>{t.name}{t.itemCategory ? ` — ${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span className="text-muted-foreground">${t.defaultPrice}</span>
|
||||
</button>
|
||||
))
|
||||
@@ -462,7 +462,7 @@ function NewRepairPage() {
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-lg">Intake Photos</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">Optional — document instrument condition at intake</p>
|
||||
<p className="text-sm text-muted-foreground">Optional — document item condition at intake</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{photos.map((photo, i) => (
|
||||
<div key={i} className="relative group">
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Route = createFileRoute('/_authenticated/repairs/templates')({
|
||||
|
||||
const columns: Column<RepairServiceTemplate>[] = [
|
||||
{ key: 'name', header: 'Name', sortable: true, render: (t) => <span className="font-medium">{t.name}</span> },
|
||||
{ key: 'instrument_type', header: 'Instrument', sortable: true, render: (t) => <>{t.instrumentType ?? '-'}</> },
|
||||
{ key: 'item_category', header: 'Item Category', sortable: true, render: (t) => <>{t.itemCategory ?? '-'}</> },
|
||||
{ key: 'size', header: 'Size', render: (t) => <>{t.size ?? '-'}</> },
|
||||
{ key: 'item_type', header: 'Type', render: (t) => <Badge variant="outline">{t.itemType.replace('_', ' ')}</Badge> },
|
||||
{ key: 'default_price', header: 'Price', sortable: true, render: (t) => <>${t.defaultPrice}</> },
|
||||
@@ -110,7 +110,7 @@ function RepairTemplatesPage() {
|
||||
function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const [instrumentType, setInstrumentType] = useState('')
|
||||
const [itemCategory, setItemCategory] = useState('')
|
||||
const [size, setSize] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [itemType, setItemType] = useState('flat_rate')
|
||||
@@ -131,7 +131,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
|
||||
function resetForm() {
|
||||
setName('')
|
||||
setInstrumentType('')
|
||||
setItemCategory('')
|
||||
setSize('')
|
||||
setDescription('')
|
||||
setItemType('flat_rate')
|
||||
@@ -144,7 +144,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
e.preventDefault()
|
||||
mutation.mutate({
|
||||
name,
|
||||
instrumentType: instrumentType || undefined,
|
||||
itemCategory: itemCategory || undefined,
|
||||
size: size || undefined,
|
||||
description: description || undefined,
|
||||
itemType,
|
||||
@@ -168,8 +168,8 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Instrument Type</Label>
|
||||
<Input value={instrumentType} onChange={(e) => setInstrumentType(e.target.value)} placeholder="e.g. Violin, Trumpet, Guitar" />
|
||||
<Label>Item Category</Label>
|
||||
<Input value={itemCategory} onChange={(e) => setItemCategory(e.target.value)} placeholder="e.g. Electronics, Appliances, Furniture" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Size</Label>
|
||||
|
||||
@@ -48,8 +48,8 @@ function LoginPage() {
|
||||
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1>
|
||||
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p>
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>LunarFront</h1>
|
||||
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Small Business Management</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -50,7 +50,7 @@ function expandPermissions(slugs: string[]): Set<string> {
|
||||
|
||||
function loadSession(): { token: string; user: User; permissions?: string[] } | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('forte-auth')
|
||||
const raw = sessionStorage.getItem('lunarfront-auth')
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
@@ -59,11 +59,11 @@ function loadSession(): { token: string; user: User; permissions?: string[] } |
|
||||
}
|
||||
|
||||
function saveSession(token: string, user: User, permissions?: string[]) {
|
||||
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, permissions }))
|
||||
sessionStorage.setItem('lunarfront-auth', JSON.stringify({ token, user, permissions }))
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
sessionStorage.removeItem('forte-auth')
|
||||
sessionStorage.removeItem('lunarfront-auth')
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => {
|
||||
|
||||
@@ -22,8 +22,8 @@ function apply(mode: Mode, colorTheme: string) {
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>((set) => {
|
||||
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system'
|
||||
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate'
|
||||
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-mode') as Mode : null) ?? 'system'
|
||||
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-color-theme') : null) ?? 'slate'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
apply(initialMode, initialColor)
|
||||
@@ -34,14 +34,14 @@ export const useThemeStore = create<ThemeState>((set) => {
|
||||
colorTheme: initialColor,
|
||||
|
||||
setMode: (mode) => {
|
||||
localStorage.setItem('forte-mode', mode)
|
||||
localStorage.setItem('lunarfront-mode', mode)
|
||||
const colorTheme = useThemeStore.getState().colorTheme
|
||||
apply(mode, colorTheme)
|
||||
set({ mode })
|
||||
},
|
||||
|
||||
setColorTheme: (name) => {
|
||||
localStorage.setItem('forte-color-theme', name)
|
||||
localStorage.setItem('lunarfront-color-theme', name)
|
||||
const mode = useThemeStore.getState().mode
|
||||
apply(mode, name)
|
||||
set({ colorTheme: name })
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface RepairTicket {
|
||||
customerName: string
|
||||
customerPhone: string | null
|
||||
inventoryUnitId: string | null
|
||||
instrumentDescription: string | null
|
||||
itemDescription: string | null
|
||||
serialNumber: string | null
|
||||
conditionIn: 'excellent' | 'good' | 'fair' | 'poor' | null
|
||||
conditionInNotes: string | null
|
||||
@@ -55,7 +55,7 @@ export interface RepairBatch {
|
||||
dueDate: string | null
|
||||
completedDate: string | null
|
||||
deliveredDate: string | null
|
||||
instrumentCount: number
|
||||
itemCount: number
|
||||
receivedCount: number
|
||||
estimatedTotal: string | null
|
||||
actualTotal: string | null
|
||||
@@ -79,7 +79,7 @@ export interface RepairNote {
|
||||
export interface RepairServiceTemplate {
|
||||
id: string
|
||||
name: string
|
||||
instrumentType: string | null
|
||||
itemCategory: string | null
|
||||
size: string | null
|
||||
description: string | null
|
||||
itemType: 'labor' | 'part' | 'flat_rate' | 'misc'
|
||||
|
||||
@@ -16,17 +16,17 @@ const pages: WikiPage[] = [
|
||||
title: 'Getting Started',
|
||||
category: 'General',
|
||||
content: `
|
||||
# Getting Started with Forte
|
||||
# Getting Started with LunarFront
|
||||
|
||||
Welcome to Forte — your music store management platform.
|
||||
Welcome to LunarFront — your small business management platform.
|
||||
|
||||
## Signing In
|
||||
|
||||
1. Open Forte in your browser
|
||||
1. Open LunarFront in your browser
|
||||
2. Enter your email and password
|
||||
3. Click **Sign in**
|
||||
|
||||
If you don't have an account, ask your store manager to create one for you.
|
||||
If you don't have an account, ask your admin to create one for you.
|
||||
|
||||
## Navigation
|
||||
|
||||
@@ -34,13 +34,13 @@ Use the sidebar on the left to navigate between sections:
|
||||
|
||||
- **Accounts** — manage customer accounts and their members
|
||||
- **Members** — find and manage individual people across all accounts
|
||||
- **Repairs** — track instrument repair tickets
|
||||
- **Repairs** — track repair tickets
|
||||
- **Repair Batches** — manage bulk school repair jobs
|
||||
- **Help** — you're here!
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you can't find what you're looking for, contact your store manager or system administrator.
|
||||
If you can't find what you're looking for, contact your admin or system administrator.
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ Every account gets a unique 6-digit number automatically. This number appears in
|
||||
content: `
|
||||
# Members
|
||||
|
||||
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a band director — anyone who takes lessons, rents instruments, or needs to be tracked.
|
||||
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a staff member — anyone who takes lessons, uses services, or needs to be tracked.
|
||||
|
||||
## Adding a Member
|
||||
|
||||
@@ -136,7 +136,7 @@ Payment methods are cards on file for an account. These are used for recurring b
|
||||
5. Optionally enter card brand, last four digits, and expiration
|
||||
6. Click **Add Payment Method**
|
||||
|
||||
**Note:** Card details are stored securely with the payment processor — Forte only keeps a reference and display info (last 4 digits, brand).
|
||||
**Note:** Card details are stored securely with the payment processor — LunarFront only keeps a reference and display info (last 4 digits, brand).
|
||||
|
||||
## Default Payment Method
|
||||
|
||||
@@ -154,7 +154,7 @@ If a payment method was migrated from an old system, it may show a "Needs Update
|
||||
content: `
|
||||
# Tax Exemptions
|
||||
|
||||
Schools, churches, and resellers may be exempt from sales tax. Forte tracks tax exemption certificates per account.
|
||||
Schools, churches, and resellers may be exempt from sales tax. LunarFront tracks tax exemption certificates per account.
|
||||
|
||||
## Adding a Tax Exemption
|
||||
|
||||
@@ -191,7 +191,7 @@ All approvals and revocations are logged with who did it and when.
|
||||
content: `
|
||||
# Identity Documents
|
||||
|
||||
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during instrument pickups, rentals, or trade-ins.
|
||||
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during pickups, rentals, or trade-ins.
|
||||
|
||||
## Adding an ID
|
||||
|
||||
@@ -225,7 +225,7 @@ If a member has multiple IDs, mark one as **Primary** — this is the one shown
|
||||
content: `
|
||||
# Users & Roles
|
||||
|
||||
Forte uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
|
||||
LunarFront uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
|
||||
|
||||
## Managing Users
|
||||
|
||||
@@ -306,16 +306,16 @@ Your preferences are saved in your browser and persist across sessions.
|
||||
content: `
|
||||
# Repairs
|
||||
|
||||
The Repairs module tracks instrument repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
|
||||
The Repairs module tracks repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
|
||||
|
||||
## Creating a Repair Ticket
|
||||
|
||||
1. Go to **Repairs** in the sidebar
|
||||
2. Click **New Repair**
|
||||
3. Search for an existing account or enter customer details manually for walk-ins
|
||||
4. Describe the instrument and the problem
|
||||
4. Describe the item and the problem
|
||||
5. Optionally add line items for the estimate (use templates for common services)
|
||||
6. Add intake photos to document the instrument's condition
|
||||
6. Add intake photos to document the item's condition
|
||||
7. Click **Create Ticket**
|
||||
|
||||
## Ticket Status Flow
|
||||
@@ -323,16 +323,16 @@ The Repairs module tracks instrument repair tickets from intake through completi
|
||||
Each ticket moves through these stages:
|
||||
|
||||
- **New** — ticket just created, not yet examined
|
||||
- **In Transit** — instrument being transported to the shop (for school pickups or shipped instruments)
|
||||
- **Intake** — instrument received, condition documented
|
||||
- **Diagnosing** — technician examining the instrument
|
||||
- **In Transit** — item being transported to the shop (for pickups or shipped items)
|
||||
- **Intake** — item received, condition documented
|
||||
- **Diagnosing** — technician examining the item
|
||||
- **Pending Approval** — estimate provided, waiting for customer OK
|
||||
- **Approved** — customer authorized the work
|
||||
- **In Progress** — actively being repaired
|
||||
- **Pending Parts** — waiting on parts order
|
||||
- **Ready** — repair complete, awaiting pickup
|
||||
- **Picked Up** — customer collected the instrument
|
||||
- **Delivered** — instrument returned via delivery (for school batches)
|
||||
- **Picked Up** — customer collected the item
|
||||
- **Delivered** — item returned via delivery (for school batches)
|
||||
|
||||
Click the status buttons on the ticket detail page to advance through the workflow. You can also click steps on the progress bar.
|
||||
|
||||
@@ -340,7 +340,7 @@ Click the status buttons on the ticket detail page to advance through the workfl
|
||||
|
||||
The ticket detail has four tabs:
|
||||
|
||||
- **Details** — customer info, instrument, condition, costs. Click **Edit** to modify.
|
||||
- **Details** — customer info, item, condition, costs. Click **Edit** to modify.
|
||||
- **Line Items** — labor, parts, flat-rate services, and misc charges. Use the template picker for common repairs.
|
||||
- **Notes** — running journal of notes. Choose **Internal** (staff only) or **Customer Visible**. You can attach photos to notes.
|
||||
- **Photos & Docs** — photos organized by repair phase (intake, in progress, completed) plus a documents section for signed approvals, quotes, and receipts.
|
||||
@@ -372,7 +372,7 @@ Templates are pre-defined common repairs (e.g. "Bow Rehair — Violin — 4/4")
|
||||
2. Click **New Template**
|
||||
3. Fill in:
|
||||
- **Name** — e.g. "Bow Rehair", "String Change", "Valve Overhaul"
|
||||
- **Instrument Type** — e.g. "Violin", "Guitar", "Trumpet"
|
||||
- **Item Category** — e.g. "Violin", "Guitar", "Trumpet"
|
||||
- **Size** — e.g. "4/4", "3/4", "Full"
|
||||
- **Type** — Labor, Part, Flat Rate, or Misc
|
||||
- **Default Price** — the customer-facing price
|
||||
@@ -400,19 +400,19 @@ When creating a ticket or adding line items, type in the **Quick Add from Templa
|
||||
content: `
|
||||
# Repair Batches
|
||||
|
||||
Batches group multiple repair tickets under one job — typically for schools bringing in many instruments at once.
|
||||
Batches group multiple repair tickets under one job — typically for schools bringing in many items at once.
|
||||
|
||||
## Creating a Batch
|
||||
|
||||
1. Go to **Repair Batches** in the sidebar
|
||||
2. Click **New Batch**
|
||||
3. Select the school's account
|
||||
4. Enter contact info, instrument count, and any notes
|
||||
4. Enter contact info, item count, and any notes
|
||||
5. Click **Create Batch**
|
||||
|
||||
## Adding Tickets to a Batch
|
||||
|
||||
When creating new repair tickets, select the batch in the form. Each instrument gets its own ticket linked to the batch.
|
||||
When creating new repair tickets, select the batch in the form. Each item gets its own ticket linked to the batch.
|
||||
|
||||
## Batch Approval
|
||||
|
||||
@@ -426,10 +426,10 @@ Only admins can approve or reject batches.
|
||||
|
||||
## Batch Status
|
||||
|
||||
- **Intake** — receiving instruments
|
||||
- **Intake** — receiving items
|
||||
- **In Progress** — work underway
|
||||
- **Completed** — all repairs done
|
||||
- **Delivered** — instruments returned to school
|
||||
- **Delivered** — items returned to customer
|
||||
|
||||
## Filtering
|
||||
|
||||
@@ -468,7 +468,7 @@ Photos appear inline in the note entry. Click to view full size.
|
||||
|
||||
The Photos & Docs tab organizes photos into categories:
|
||||
|
||||
- **Intake Photos** — document instrument condition when received
|
||||
- **Intake Photos** — document item condition when received
|
||||
- **Work in Progress** — during the repair
|
||||
- **Completed** — final result after repair
|
||||
- **Documents** — signed approvals, quotes, receipts (accepts PDFs)
|
||||
|
||||
@@ -7,9 +7,9 @@ import { createClient } from './lib/client.js'
|
||||
// --- Config ---
|
||||
const DB_HOST = process.env.DB_HOST ?? 'localhost'
|
||||
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
|
||||
const DB_USER = process.env.DB_USER ?? 'forte'
|
||||
const DB_PASS = process.env.DB_PASS ?? 'forte'
|
||||
const TEST_DB = 'forte_api_test'
|
||||
const DB_USER = process.env.DB_USER ?? 'lunarfront'
|
||||
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
|
||||
const TEST_DB = 'lunarfront_api_test'
|
||||
const TEST_PORT = 8001
|
||||
const BASE_URL = `http://localhost:${TEST_PORT}`
|
||||
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
|
||||
@@ -60,7 +60,7 @@ async function setupDatabase() {
|
||||
`)
|
||||
|
||||
// Seed company + location (company table stays as store settings)
|
||||
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Music Co.', 'America/Chicago')`
|
||||
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
|
||||
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
|
||||
|
||||
// Seed lookup tables
|
||||
@@ -96,8 +96,8 @@ async function setupDatabase() {
|
||||
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog, stock tracking, and unit management', enabled: true },
|
||||
{ slug: 'pos', name: 'Point of Sale', description: 'Sales transactions, cash drawer, and receipts', enabled: true },
|
||||
{ slug: 'repairs', name: 'Repairs', description: 'Repair ticket management, batches, and service templates', enabled: true },
|
||||
{ slug: 'rentals', name: 'Rentals', description: 'Instrument rental agreements and billing', enabled: false },
|
||||
{ slug: 'lessons', name: 'Lessons', description: 'Lesson scheduling, instructor management, and billing', enabled: false },
|
||||
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
|
||||
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling, staff management, and billing', enabled: false },
|
||||
{ slug: 'files', name: 'Files', description: 'Shared file storage with folder organization', enabled: true },
|
||||
{ slug: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
|
||||
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false },
|
||||
@@ -134,7 +134,7 @@ async function startBackend(): Promise<Subprocess> {
|
||||
HOST: '0.0.0.0',
|
||||
NODE_ENV: 'development',
|
||||
LOG_LEVEL: 'error',
|
||||
STORAGE_LOCAL_PATH: '/tmp/forte-test-files',
|
||||
STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
|
||||
},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
@@ -170,7 +170,7 @@ async function registerTestUser(): Promise<string> {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
email: 'test@forte.dev',
|
||||
email: 'test@lunarfront.dev',
|
||||
password: testPassword,
|
||||
firstName: 'Test',
|
||||
lastName: 'Runner',
|
||||
@@ -193,7 +193,7 @@ async function registerTestUser(): Promise<string> {
|
||||
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@forte.dev', password: testPassword }),
|
||||
body: JSON.stringify({ email: 'test@lunarfront.dev', password: testPassword }),
|
||||
})
|
||||
const loginData = await loginRes.json() as { token?: string }
|
||||
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)
|
||||
|
||||
@@ -304,7 +304,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
|
||||
t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => {
|
||||
// Get current user ID from the users list
|
||||
const usersRes = await t.api.get('/v1/users')
|
||||
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@forte.dev')
|
||||
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@lunarfront.dev')
|
||||
t.assert.ok(currentUser)
|
||||
|
||||
const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false })
|
||||
|
||||
@@ -7,8 +7,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const res = await t.api.post('/v1/repair-tickets', {
|
||||
customerName: 'Walk-In Customer',
|
||||
customerPhone: '555-0100',
|
||||
instrumentDescription: 'Yamaha Trumpet',
|
||||
problemDescription: 'Stuck valve, needs cleaning',
|
||||
itemDescription: 'Samsung Galaxy S24',
|
||||
problemDescription: 'Cracked screen, touch not working',
|
||||
conditionIn: 'fair',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -25,8 +25,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const res = await t.api.post('/v1/repair-tickets', {
|
||||
customerName: 'Repair Customer',
|
||||
accountId: acct.data.id,
|
||||
problemDescription: 'Broken bridge on violin',
|
||||
instrumentDescription: 'Student Violin 4/4',
|
||||
problemDescription: 'Screen flickering intermittently',
|
||||
itemDescription: 'Dell XPS 15 Laptop',
|
||||
conditionIn: 'poor',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -164,12 +164,12 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
t.assert.ok(res.data.data.some((t: { customerName: string }) => t.customerName === 'Searchable Trumpet Guy'))
|
||||
})
|
||||
|
||||
t.test('searches tickets by instrument description', { tags: ['tickets', 'search'] }, async () => {
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Instrument Search', problemDescription: 'Test', instrumentDescription: 'Selmer Mark VI Saxophone' })
|
||||
t.test('searches tickets by item description', { tags: ['tickets', 'search'] }, async () => {
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Item Search', problemDescription: 'Test', itemDescription: 'Samsung Galaxy S24 Ultra' })
|
||||
|
||||
const res = await t.api.get('/v1/repair-tickets', { q: 'Mark VI' })
|
||||
const res = await t.api.get('/v1/repair-tickets', { q: 'Galaxy S24' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.some((t: { instrumentDescription: string }) => t.instrumentDescription?.includes('Mark VI')))
|
||||
t.assert.ok(res.data.data.some((t: { itemDescription: string }) => t.itemDescription?.includes('Galaxy S24')))
|
||||
})
|
||||
|
||||
t.test('sorts tickets by customer name descending', { tags: ['tickets', 'sort'] }, async () => {
|
||||
@@ -289,7 +289,7 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Customer Note', problemDescription: 'Test' })
|
||||
|
||||
const res = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, {
|
||||
content: 'Your instrument is ready for pickup',
|
||||
content: 'Your item is ready for pickup',
|
||||
visibility: 'customer',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -341,17 +341,17 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
const res = await t.api.post('/v1/repair-batches', {
|
||||
accountId: acct.data.id,
|
||||
contactName: 'Band Director',
|
||||
contactName: 'IT Director',
|
||||
contactPhone: '555-0200',
|
||||
instrumentCount: 15,
|
||||
notes: 'Annual instrument checkup',
|
||||
itemCount: 15,
|
||||
notes: 'Annual equipment checkup',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.ok(res.data.batchNumber)
|
||||
t.assert.equal(res.data.batchNumber.length, 6)
|
||||
t.assert.equal(res.data.status, 'intake')
|
||||
t.assert.equal(res.data.approvalStatus, 'pending')
|
||||
t.assert.equal(res.data.instrumentCount, 15)
|
||||
t.assert.equal(res.data.itemCount, 15)
|
||||
})
|
||||
|
||||
t.test('returns 404 for missing batch', { tags: ['batches', 'read'] }, async () => {
|
||||
@@ -377,19 +377,19 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
t.test('updates a batch', { tags: ['batches', 'update'] }, async () => {
|
||||
const acct = await t.api.post('/v1/accounts', { name: 'Update Batch School', billingMode: 'consolidated' })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 5 })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 5 })
|
||||
|
||||
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { instrumentCount: 10, contactName: 'Updated Director' })
|
||||
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { itemCount: 10, contactName: 'Updated Director' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.contactName, 'Updated Director')
|
||||
})
|
||||
|
||||
t.test('adds tickets to a batch and lists them', { tags: ['batches', 'tickets'] }, async () => {
|
||||
const acct = await t.api.post('/v1/accounts', { name: 'Batch Tickets School', billingMode: 'consolidated' })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 2 })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 2 })
|
||||
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Flute pads', instrumentDescription: 'Flute' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Clarinet cork', instrumentDescription: 'Clarinet' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Screen cracked', itemDescription: 'Chromebook #1' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Battery dead', itemDescription: 'Chromebook #2' })
|
||||
|
||||
const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 })
|
||||
t.assert.status(tickets, 200)
|
||||
@@ -437,36 +437,36 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
|
||||
const res = await t.api.post('/v1/repair-service-templates', {
|
||||
name: 'Bow Rehair',
|
||||
instrumentType: 'Violin',
|
||||
size: '4/4',
|
||||
name: 'Screen Repair',
|
||||
itemCategory: 'Electronics',
|
||||
size: 'Phone',
|
||||
itemType: 'flat_rate',
|
||||
defaultPrice: 65,
|
||||
defaultCost: 15,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, 'Bow Rehair')
|
||||
t.assert.equal(res.data.instrumentType, 'Violin')
|
||||
t.assert.equal(res.data.size, '4/4')
|
||||
t.assert.equal(res.data.name, 'Screen Repair')
|
||||
t.assert.equal(res.data.itemCategory, 'Electronics')
|
||||
t.assert.equal(res.data.size, 'Phone')
|
||||
t.assert.equal(res.data.defaultPrice, '65.00')
|
||||
t.assert.equal(res.data.defaultCost, '15.00')
|
||||
})
|
||||
|
||||
t.test('lists service templates with search', { tags: ['templates', 'read'] }, async () => {
|
||||
await t.api.post('/v1/repair-service-templates', { name: 'String Change', instrumentType: 'Guitar', defaultPrice: 25 })
|
||||
await t.api.post('/v1/repair-service-templates', { name: 'Battery Replacement', itemCategory: 'Electronics', defaultPrice: 25 })
|
||||
|
||||
const res = await t.api.get('/v1/repair-service-templates', { q: 'String', limit: 100 })
|
||||
const res = await t.api.get('/v1/repair-service-templates', { q: 'Battery', limit: 100 })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'String Change'))
|
||||
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'Battery Replacement'))
|
||||
t.assert.ok(res.data.pagination)
|
||||
})
|
||||
|
||||
t.test('updates a service template', { tags: ['templates', 'update'] }, async () => {
|
||||
const created = await t.api.post('/v1/repair-service-templates', { name: 'Pad Replace', defaultPrice: 30 })
|
||||
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, instrumentType: 'Clarinet' })
|
||||
const created = await t.api.post('/v1/repair-service-templates', { name: 'Tune-Up', defaultPrice: 30 })
|
||||
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, itemCategory: 'Bicycles' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.defaultPrice, '35.00')
|
||||
t.assert.equal(res.data.instrumentType, 'Clarinet')
|
||||
t.assert.equal(res.data.itemCategory, 'Bicycles')
|
||||
})
|
||||
|
||||
t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {
|
||||
|
||||
@@ -96,14 +96,14 @@ suite('Vault', { tags: ['vault'] }, (t) => {
|
||||
|
||||
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
|
||||
name: 'Store WiFi',
|
||||
username: 'ForteMusic',
|
||||
username: 'DemoUser',
|
||||
url: 'http://192.168.1.1',
|
||||
notes: 'Router admin panel',
|
||||
secret: 'supersecretpassword123',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, 'Store WiFi')
|
||||
t.assert.equal(res.data.username, 'ForteMusic')
|
||||
t.assert.equal(res.data.username, 'DemoUser')
|
||||
t.assert.equal(res.data.hasSecret, true)
|
||||
// Secret value should NOT be in the response
|
||||
t.assert.falsy(res.data.encryptedValue)
|
||||
|
||||
@@ -27,7 +27,7 @@ async function dav(
|
||||
|
||||
suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => {
|
||||
// Use the same test user created by the test runner
|
||||
const email = 'test@forte.dev'
|
||||
const email = 'test@lunarfront.dev'
|
||||
const password = 'testpassword1234'
|
||||
const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64')
|
||||
const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64')
|
||||
|
||||
@@ -5,6 +5,6 @@ export default defineConfig({
|
||||
out: './src/db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte',
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/backend",
|
||||
"name": "@lunarfront/backend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -13,6 +13,8 @@
|
||||
"db:generate": "bunx drizzle-kit generate",
|
||||
"db:migrate": "bunx drizzle-kit migrate",
|
||||
"db:seed-dev": "bun run src/db/seeds/dev-seed.ts",
|
||||
"db:seed-music": "bun run src/db/seeds/music-store-seed.ts",
|
||||
"db:seed-reset-repairs": "bun run src/db/seeds/reset-repairs.ts",
|
||||
"db:seed": "bun run src/db/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,7 +22,7 @@
|
||||
"@fastify/jwt": "^9",
|
||||
"@fastify/multipart": "^9.4.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@forte/shared": "workspace:*",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"bcrypt": "^6",
|
||||
"drizzle-orm": "^0.38",
|
||||
"fastify": "^5",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Generalize music-specific column names to generic terminology
|
||||
|
||||
-- repair_ticket: instrument_description -> item_description
|
||||
ALTER TABLE repair_ticket RENAME COLUMN instrument_description TO item_description;
|
||||
|
||||
-- repair_batch: instrument_count -> item_count
|
||||
ALTER TABLE repair_batch RENAME COLUMN instrument_count TO item_count;
|
||||
|
||||
-- repair_service_template: instrument_type -> item_category
|
||||
ALTER TABLE repair_service_template RENAME COLUMN instrument_type TO item_category;
|
||||
|
||||
-- Update module descriptions to be industry-agnostic
|
||||
UPDATE module_config SET description = 'Rental agreements and billing' WHERE slug = 'rentals';
|
||||
UPDATE module_config SET description = 'Scheduling, staff management, and billing' WHERE slug = 'lessons';
|
||||
@@ -190,6 +190,13 @@
|
||||
"when": 1774860000000,
|
||||
"tag": "0026_modules",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1774870000000,
|
||||
"tag": "0027_generalize_terminology",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export const repairBatches = pgTable('repair_batch', {
|
||||
dueDate: timestamp('due_date', { withTimezone: true }),
|
||||
completedDate: timestamp('completed_date', { withTimezone: true }),
|
||||
deliveredDate: timestamp('delivered_date', { withTimezone: true }),
|
||||
instrumentCount: integer('instrument_count').notNull().default(0),
|
||||
itemCount: integer('item_count').notNull().default(0),
|
||||
receivedCount: integer('received_count').notNull().default(0),
|
||||
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
|
||||
actualTotal: numeric('actual_total', { precision: 10, scale: 2 }),
|
||||
@@ -101,7 +101,7 @@ export const repairTickets = pgTable('repair_ticket', {
|
||||
customerName: varchar('customer_name', { length: 255 }).notNull(),
|
||||
customerPhone: varchar('customer_phone', { length: 50 }),
|
||||
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
|
||||
instrumentDescription: text('instrument_description'),
|
||||
itemDescription: text('item_description'),
|
||||
serialNumber: varchar('serial_number', { length: 255 }),
|
||||
conditionIn: repairConditionInEnum('condition_in'),
|
||||
conditionInNotes: text('condition_in_notes'),
|
||||
@@ -158,7 +158,7 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
|
||||
export const repairServiceTemplates = pgTable('repair_service_template', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
instrumentType: varchar('instrument_type', { length: 100 }),
|
||||
itemCategory: varchar('item_category', { length: 100 }),
|
||||
size: varchar('size', { length: 50 }),
|
||||
description: text('description'),
|
||||
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),
|
||||
|
||||
@@ -7,7 +7,7 @@ const DEV_LOCATION_ID = '00000000-0000-0000-0000-000000000010'
|
||||
|
||||
async function seed() {
|
||||
const connectionString =
|
||||
process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
|
||||
process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
|
||||
const sql = postgres(connectionString)
|
||||
const db = drizzle(sql)
|
||||
|
||||
@@ -17,7 +17,7 @@ async function seed() {
|
||||
.insert(companies)
|
||||
.values({
|
||||
id: DEV_COMPANY_ID,
|
||||
name: 'Dev Music Co.',
|
||||
name: 'Dev Store',
|
||||
timezone: 'America/Chicago',
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import postgres from 'postgres'
|
||||
|
||||
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
|
||||
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
|
||||
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
|
||||
|
||||
const sql = postgres(DB_URL)
|
||||
@@ -17,7 +17,7 @@ async function seed() {
|
||||
// Create company and location if they don't exist
|
||||
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
|
||||
if (!company) {
|
||||
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Forte Music Store', 'America/Chicago')`
|
||||
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Demo Store', 'America/Chicago')`
|
||||
await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
|
||||
console.log(' Created company and location')
|
||||
|
||||
@@ -39,16 +39,16 @@ async function seed() {
|
||||
}
|
||||
|
||||
// --- Admin user (if not exists) ---
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'`
|
||||
if (!adminUser) {
|
||||
const bcrypt = await import('bcrypt')
|
||||
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
|
||||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
|
||||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
}
|
||||
console.log(' Created admin user: admin@forte.dev / admin1234')
|
||||
console.log(' Created admin user: admin@lunarfront.dev / admin1234')
|
||||
} else {
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
@@ -61,11 +61,11 @@ async function seed() {
|
||||
const accounts = [
|
||||
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' },
|
||||
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' },
|
||||
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200' },
|
||||
{ name: 'Garcia Music Studio', email: 'garcia@studio.com', phone: '555-0103' },
|
||||
{ name: 'Lincoln High School', email: 'office@lincoln.edu', phone: '555-0200' },
|
||||
{ name: 'Garcia Workshop', email: 'garcia@studio.com', phone: '555-0103' },
|
||||
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' },
|
||||
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105' },
|
||||
{ name: 'Westside Church', email: 'music@westsidechurch.org', phone: '555-0300' },
|
||||
{ name: 'Westside Church', email: 'admin@westsidechurch.org', phone: '555-0300' },
|
||||
{ name: 'Oak Elementary', email: 'office@oakelementary.edu', phone: '555-0201' },
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ async function seed() {
|
||||
{ accountName: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true },
|
||||
{ accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' },
|
||||
{ accountName: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true },
|
||||
{ accountName: 'Garcia Music Studio', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
|
||||
{ accountName: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
|
||||
{ accountName: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com' },
|
||||
{ accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' },
|
||||
]
|
||||
@@ -105,39 +105,37 @@ async function seed() {
|
||||
|
||||
// --- Repair Service Templates ---
|
||||
const templates = [
|
||||
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' },
|
||||
{ name: 'String Change', instrumentType: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' },
|
||||
{ name: 'String Change', instrumentType: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' },
|
||||
{ name: 'String Change', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' },
|
||||
{ name: 'Valve Overhaul', instrumentType: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null },
|
||||
{ name: 'Pad Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '120.00', cost: '30.00' },
|
||||
{ name: 'Pad Replacement', instrumentType: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' },
|
||||
{ name: 'Cork Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' },
|
||||
{ name: 'Slide Repair', instrumentType: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null },
|
||||
{ name: 'Bridge Setup', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' },
|
||||
{ name: 'Guitar Setup', instrumentType: 'Guitar', size: null, itemType: 'flat_rate', price: '65.00', cost: '5.00' },
|
||||
{ name: 'Dent Removal', instrumentType: 'Brass', size: null, itemType: 'labor', price: '50.00', cost: null },
|
||||
{ name: 'General Cleaning', instrumentType: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '89.00', cost: '25.00' },
|
||||
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Tablet', itemType: 'flat_rate', price: '129.00', cost: '45.00' },
|
||||
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '59.00', cost: '15.00' },
|
||||
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Laptop', itemType: 'flat_rate', price: '99.00', cost: '35.00' },
|
||||
{ name: 'Tune-Up', itemCategory: 'Bicycles', size: 'Standard', itemType: 'flat_rate', price: '65.00', cost: '10.00' },
|
||||
{ name: 'Brake Adjustment', itemCategory: 'Bicycles', size: null, itemType: 'flat_rate', price: '35.00', cost: '5.00' },
|
||||
{ name: 'Blade Sharpening', itemCategory: 'Tools', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
|
||||
{ name: 'Motor Repair', itemCategory: 'Appliances', size: null, itemType: 'labor', price: '85.00', cost: null },
|
||||
{ name: 'Zipper Replacement', itemCategory: 'Clothing', size: null, itemType: 'flat_rate', price: '25.00', cost: '5.00' },
|
||||
{ name: 'Sole Replacement', itemCategory: 'Footwear', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
|
||||
{ name: 'Watch Battery', itemCategory: 'Watches', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
|
||||
{ name: 'Furniture Refinishing', itemCategory: 'Furniture', size: null, itemType: 'labor', price: '150.00', cost: null },
|
||||
{ name: 'Diagnostic Check', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
{ name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
]
|
||||
|
||||
for (const t of templates) {
|
||||
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
|
||||
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
|
||||
if (existing.length > 0) continue
|
||||
await sql`INSERT INTO repair_service_template (name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
|
||||
console.log(` Template: ${t.name} ${t.instrumentType ?? ''} ${t.size ?? ''}`)
|
||||
await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
|
||||
console.log(` Template: ${t.name} ${t.itemCategory ?? ''} ${t.size ?? ''}`)
|
||||
}
|
||||
|
||||
// --- Repair Tickets ---
|
||||
const tickets = [
|
||||
{ customer: 'Mike Thompson', instrument: 'Fender Stratocaster', serial: 'US22-045891', problem: 'Fret buzz on 3rd and 5th fret, needs setup', condition: 'good', status: 'in_progress', estimate: '65.00' },
|
||||
{ customer: 'Emily Chen', instrument: 'Yamaha YTR-2330 Trumpet', serial: 'YTR-78432', problem: 'Stuck 2nd valve, sluggish action on all valves', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
|
||||
{ customer: 'David Smith', instrument: 'Stradivarius Copy Violin', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00' },
|
||||
{ customer: 'Carlos Garcia', instrument: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null },
|
||||
{ customer: 'Lisa Johnson', instrument: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', estimate: null },
|
||||
{ customer: 'Walk-In Customer', instrument: 'Unknown Flute', serial: null, problem: 'Customer says it squeaks on high notes', condition: 'fair', status: 'intake', estimate: null },
|
||||
{ customer: 'Mike Thompson', item: 'Samsung Galaxy S24', serial: 'IMEI-354789102', problem: 'Cracked screen, touch not responsive in bottom half', condition: 'good', status: 'in_progress', estimate: '89.00' },
|
||||
{ customer: 'Emily Chen', item: 'HP LaserJet Pro M404', serial: 'HP-CNB3K12345', problem: 'Paper jam sensor error, won\'t feed from tray 2', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
|
||||
{ customer: 'David Smith', item: 'Trek Marlin 7 Mountain Bike', serial: null, problem: 'Rear derailleur bent, chain skipping gears', condition: 'fair', status: 'ready', estimate: '65.00' },
|
||||
{ customer: 'Carlos Garcia', item: 'KitchenAid Stand Mixer KSM150', serial: 'W10807813', problem: 'Motor making grinding noise at low speeds', condition: 'good', status: 'new', estimate: null },
|
||||
{ customer: 'Lisa Johnson', item: 'Apple MacBook Pro 14"', serial: null, problem: 'Battery draining rapidly, trackpad click intermittent', condition: 'poor', status: 'diagnosing', estimate: null },
|
||||
{ customer: 'Walk-In Customer', item: 'Leather Work Boots', serial: null, problem: 'Sole separating from upper on both shoes', condition: 'fair', status: 'intake', estimate: null },
|
||||
]
|
||||
|
||||
for (const t of tickets) {
|
||||
@@ -145,8 +143,8 @@ async function seed() {
|
||||
if (existing.length > 0) continue
|
||||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||||
const acctId = acctIds[t.customer] ?? null
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
|
||||
console.log(` Ticket: ${t.customer} — ${t.instrument} [${t.status}]`)
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
|
||||
console.log(` Ticket: ${t.customer} — ${t.item} [${t.status}]`)
|
||||
}
|
||||
|
||||
// --- Repair Batch ---
|
||||
@@ -154,22 +152,22 @@ async function seed() {
|
||||
if (batchExists.length === 0) {
|
||||
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
|
||||
const schoolId = acctIds['Lincoln High School']
|
||||
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
|
||||
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual equipment checkup — 5 items', 'intake') RETURNING id`
|
||||
|
||||
const batchTickets = [
|
||||
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' },
|
||||
{ instrument: 'Student Clarinet #1', problem: 'Keys sticking, cork dried out', condition: 'fair' },
|
||||
{ instrument: 'Student Clarinet #2', problem: 'Barrel crack, needs assessment', condition: 'poor' },
|
||||
{ instrument: 'Student Trumpet', problem: 'Valve oil needed, general checkup', condition: 'good' },
|
||||
{ instrument: 'Student Trombone', problem: 'Slide dent, sluggish movement', condition: 'fair' },
|
||||
{ item: 'Chromebook #101', problem: 'Screen flickering, hinge loose', condition: 'fair' },
|
||||
{ item: 'Chromebook #102', problem: 'Keyboard unresponsive, several keys stuck', condition: 'fair' },
|
||||
{ item: 'Projector — Epson EB-X51', problem: 'Lamp dim, color wheel noise', condition: 'poor' },
|
||||
{ item: 'Label Printer — Dymo 450', problem: 'Feed mechanism jammed', condition: 'good' },
|
||||
{ item: 'PA Speaker — JBL EON715', problem: 'Crackling at high volume', condition: 'fair' },
|
||||
]
|
||||
|
||||
for (const bt of batchTickets) {
|
||||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
|
||||
console.log(` Batch ticket: ${bt.instrument}`)
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')`
|
||||
console.log(` Batch ticket: ${bt.item}`)
|
||||
}
|
||||
console.log(` Batch: Lincoln High School — 5 instruments`)
|
||||
console.log(` Batch: Lincoln High School — 5 items`)
|
||||
}
|
||||
|
||||
console.log('\nDev seed complete!')
|
||||
|
||||
@@ -20,14 +20,14 @@ export const SYSTEM_PERMISSIONS = [
|
||||
{ slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' },
|
||||
|
||||
// Rentals
|
||||
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, fleet, billing' },
|
||||
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage fleet' },
|
||||
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, inventory, billing' },
|
||||
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage rental inventory' },
|
||||
{ slug: 'rentals.admin', domain: 'rentals', action: 'admin', description: 'Override terms, adjust equity, cancel contracts' },
|
||||
|
||||
// Lessons
|
||||
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View lesson schedules, enrollments, attendance' },
|
||||
// Lessons / Scheduling
|
||||
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View schedules, enrollments, attendance' },
|
||||
{ slug: 'lessons.edit', domain: 'lessons', action: 'edit', description: 'Manage scheduling, enrollment, attendance' },
|
||||
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure lesson settings, manage instructors' },
|
||||
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure scheduling settings, manage staff' },
|
||||
|
||||
// Repairs
|
||||
{ slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' },
|
||||
|
||||
@@ -80,7 +80,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
|
||||
const authHeader = request.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Authentication required')
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
const colonIndex = decoded.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
@@ -103,14 +103,14 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.passwordHash)
|
||||
if (!valid) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
TaxExemptionUpdateSchema,
|
||||
MemberIdentifierCreateSchema,
|
||||
MemberIdentifierUpdateSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
AccountService,
|
||||
MemberService,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
|
||||
import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas'
|
||||
import { users } from '../../db/schema/users.js'
|
||||
|
||||
const SALT_ROUNDS = 10
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SupplierCreateSchema,
|
||||
SupplierUpdateSchema,
|
||||
PaginationSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { CategoryService, SupplierService } from '../../services/inventory.service.js'
|
||||
|
||||
export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { LookupCreateSchema, LookupUpdateSchema } from '@forte/shared/schemas'
|
||||
import { LookupCreateSchema, LookupUpdateSchema } from '@lunarfront/shared/schemas'
|
||||
import { UnitStatusService, ItemConditionService } from '../../services/lookup.service.js'
|
||||
import { ConflictError, ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
InventoryUnitCreateSchema,
|
||||
InventoryUnitUpdateSchema,
|
||||
PaginationSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { ProductService, InventoryUnitService } from '../../services/product.service.js'
|
||||
|
||||
export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { eq, count, sql, type Column } from 'drizzle-orm'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { RbacService } from '../../services/rbac.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
import { users } from '../../db/schema/users.js'
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
RepairNoteCreateSchema,
|
||||
RepairServiceTemplateCreateSchema,
|
||||
RepairServiceTemplateUpdateSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
|
||||
|
||||
export const repairRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import multipart from '@fastify/multipart'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { StorageFolderService, StorageFileService, StoragePermissionService } from '../../services/storage.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { VaultKeyService, VaultPermissionService, VaultCategoryService, VaultEntryService } from '../../services/vault.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import type {
|
||||
TaxExemptionCreateInput,
|
||||
TaxExemptionUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { isMinor, normalizeStateCode } from '@lunarfront/shared/utils'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SupplierCreateInput,
|
||||
SupplierUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SYSTEM_UNIT_STATUSES,
|
||||
SYSTEM_ITEM_CONDITIONS,
|
||||
} from '../db/schema/lookups.js'
|
||||
import type { LookupCreateInput, LookupUpdateInput } from '@forte/shared/schemas'
|
||||
import type { LookupCreateInput, LookupUpdateInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
function createLookupService(
|
||||
table: typeof inventoryUnitStatuses | typeof itemConditions,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
InventoryUnitCreateInput,
|
||||
InventoryUnitUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq, and, inArray, count, type Column } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
|
||||
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
|
||||
import { ForbiddenError } from '../lib/errors.js'
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
RepairServiceTemplateCreateInput,
|
||||
RepairServiceTemplateUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
@@ -59,7 +59,7 @@ export const RepairTicketService = {
|
||||
locationId: input.locationId,
|
||||
repairBatchId: input.repairBatchId,
|
||||
inventoryUnitId: input.inventoryUnitId,
|
||||
instrumentDescription: input.instrumentDescription,
|
||||
itemDescription: input.itemDescription,
|
||||
serialNumber: input.serialNumber,
|
||||
conditionIn: input.conditionIn,
|
||||
conditionInNotes: input.conditionInNotes,
|
||||
@@ -101,7 +101,7 @@ export const RepairTicketService = {
|
||||
repairTickets.ticketNumber,
|
||||
repairTickets.customerName,
|
||||
repairTickets.customerPhone,
|
||||
repairTickets.instrumentDescription,
|
||||
repairTickets.itemDescription,
|
||||
repairTickets.serialNumber,
|
||||
])
|
||||
if (search) conditions.push(search)
|
||||
@@ -151,7 +151,7 @@ export const RepairTicketService = {
|
||||
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairTickets.repairBatchId, batchId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
@@ -292,7 +292,7 @@ export const RepairBatchService = {
|
||||
contactEmail: input.contactEmail,
|
||||
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
|
||||
dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
|
||||
instrumentCount: input.instrumentCount,
|
||||
itemCount: input.itemCount,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returning()
|
||||
@@ -391,7 +391,7 @@ export const RepairServiceTemplateService = {
|
||||
.insert(repairServiceTemplates)
|
||||
.values({
|
||||
name: input.name,
|
||||
instrumentType: input.instrumentType,
|
||||
itemCategory: input.itemCategory,
|
||||
size: input.size,
|
||||
description: input.description,
|
||||
itemType: input.itemType,
|
||||
@@ -406,13 +406,13 @@ export const RepairServiceTemplateService = {
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(repairServiceTemplates.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.itemCategory, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: repairServiceTemplates.name,
|
||||
instrument_type: repairServiceTemplates.instrumentType,
|
||||
item_category: repairServiceTemplates.itemCategory,
|
||||
default_price: repairServiceTemplates.defaultPrice,
|
||||
sort_order: repairServiceTemplates.sortOrder,
|
||||
created_at: repairServiceTemplates.createdAt,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
|
||||
import type { StorageProvider } from '../storage/index.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
const MAX_PARENT_DEPTH = 50
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js'
|
||||
import { userRoles } from '../db/schema/rbac.js'
|
||||
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm'
|
||||
import type { PgSelect } from 'drizzle-orm/pg-core'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
/**
|
||||
* Apply pagination (offset + limit) to a Drizzle query.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/shared",
|
||||
"name": "@lunarfront/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -36,7 +36,7 @@ export const RepairTicketCreateSchema = z.object({
|
||||
locationId: opt(z.string().uuid()),
|
||||
repairBatchId: opt(z.string().uuid()),
|
||||
inventoryUnitId: opt(z.string().uuid()),
|
||||
instrumentDescription: opt(z.string()),
|
||||
itemDescription: opt(z.string()),
|
||||
serialNumber: opt(z.string().max(255)),
|
||||
conditionIn: opt(RepairConditionIn),
|
||||
conditionInNotes: opt(z.string()),
|
||||
@@ -84,7 +84,7 @@ export const RepairBatchCreateSchema = z.object({
|
||||
contactEmail: opt(z.string().email()),
|
||||
pickupDate: opt(z.string()),
|
||||
dueDate: opt(z.string()),
|
||||
instrumentCount: z.coerce.number().int().min(0).default(0),
|
||||
itemCount: z.coerce.number().int().min(0).default(0),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type RepairBatchCreateInput = z.infer<typeof RepairBatchCreateSchema>
|
||||
@@ -112,7 +112,7 @@ export type RepairNoteCreateInput = z.infer<typeof RepairNoteCreateSchema>
|
||||
|
||||
export const RepairServiceTemplateCreateSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
instrumentType: opt(z.string().max(100)),
|
||||
itemCategory: opt(z.string().max(100)),
|
||||
size: opt(z.string().max(50)),
|
||||
description: opt(z.string()),
|
||||
itemType: RepairLineItemType.default('flat_rate'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @forte/shared types
|
||||
// @lunarfront/shared types
|
||||
// Domain types will be added as each domain is implemented
|
||||
|
||||
export type StoreId = string
|
||||
|
||||
Reference in New Issue
Block a user