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:
Ryan Moon
2026-03-30 08:51:54 -05:00
parent 535446696c
commit 9400828f62
84 changed files with 390 additions and 820 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 ---

View File

@@ -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 ---

View File

@@ -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,

View File

@@ -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

View File

@@ -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 ---

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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) }

View File

@@ -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

View File

@@ -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(

View File

@@ -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' })

View File

@@ -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',

View File

@@ -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>

View File

@@ -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>
))}

View File

@@ -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',

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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) => {

View File

@@ -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 })

View File

@@ -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'

View File

@@ -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)