feat: receipt customization settings tab with header, footer, policy, social

- New Receipt tab in Settings page with editable fields
- receipt_header: text below logo (e.g. tagline)
- receipt_footer: thank you message
- receipt_return_policy: return policy text
- receipt_social: website/social media
- All stored in app_config, rendered on printed receipts
- Seeded in migration with empty defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 21:28:37 +00:00
parent 0aa9345c27
commit 8820a56a51
4 changed files with 157 additions and 8 deletions

View File

@@ -63,6 +63,20 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
const QUICK_AMOUNTS = [1, 5, 10, 20, 50, 100]
// Fetch receipt config
interface AppConfigEntry { key: string; value: string | null }
const { data: configData } = useQuery({
queryKey: ['config'],
queryFn: () => api.get<{ data: AppConfigEntry[] }>('/v1/config'),
enabled: !!result?.id,
})
const receiptConfig = {
header: configData?.data?.find((c) => c.key === 'receipt_header')?.value || undefined,
footer: configData?.data?.find((c) => c.key === 'receipt_footer')?.value || undefined,
returnPolicy: configData?.data?.find((c) => c.key === 'receipt_return_policy')?.value || undefined,
social: configData?.data?.find((c) => c.key === 'receipt_social')?.value || undefined,
}
// Fetch full receipt data after completion
const { data: receiptData } = useQuery({
queryKey: ['pos', 'receipt', result?.id],
@@ -91,7 +105,7 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
</Button>
</div>
<div className="print:block">
<POSReceipt data={receiptData} footerText="Thank you for your business!" />
<POSReceipt data={receiptData} config={receiptConfig} />
</div>
</DialogContent>
</Dialog>

View File

@@ -45,10 +45,18 @@ interface ReceiptData {
}
}
interface ReceiptConfig {
header?: string
footer?: string
returnPolicy?: string
social?: string
}
interface POSReceiptProps {
data: ReceiptData
size?: 'thermal' | 'full'
footerText?: string
config?: ReceiptConfig
}
function useStoreLogo(companyId?: string) {
@@ -91,7 +99,7 @@ function useStoreLogo(companyId?: string) {
return logoSrc
}
export function POSReceipt({ data, size = 'thermal', footerText }: POSReceiptProps) {
export function POSReceipt({ data, size = 'thermal', footerText, config }: POSReceiptProps) {
const barcodeRef = useRef<SVGSVGElement>(null)
const { transaction: txn, company, location } = data
const isThermal = size === 'thermal'
@@ -146,6 +154,7 @@ export function POSReceipt({ data, size = 'thermal', footerText }: POSReceiptPro
</>
)}
{phone && <div>{phone}</div>}
{config?.header && <div className="mt-1 text-gray-600">{config.header}</div>}
</div>
{/* Transaction info */}
@@ -234,8 +243,14 @@ export function POSReceipt({ data, size = 'thermal', footerText }: POSReceiptPro
</div>
{/* Footer */}
{footerText && (
<div className="text-center text-gray-500 pb-2">{footerText}</div>
{(config?.footer || footerText) && (
<div className="text-center text-gray-500 pb-1">{config?.footer || footerText}</div>
)}
{config?.returnPolicy && (
<div className="text-center text-gray-400 text-[10px] pb-1">{config.returnPolicy}</div>
)}
{config?.social && (
<div className="text-center text-gray-500 pb-2">{config.social}</div>
)}
</div>
)