feat: improve invoice view responsiveness and settings UX

- Replace custom invoice items table with responsive DataTable component
- Fix server/client component error by creating InvoiceItemsTable client
  component
- Merge danger zone with actions sidebar and use destructive button
  variant
- Standardize button text sizing across all action buttons
- Remove false claims from homepage (testimonials, ratings, fake user
  counts)
- Focus homepage messaging on freelancers with honest feature
  descriptions
- Fix dark mode support throughout app by replacing hard-coded colors
  with semantic classes
- Remove aggressive red styling from settings, add subtle red accents
  only
- Align import/export buttons and improve delete confirmation UX
- Update dark mode background to have subtle green tint instead of pure
  black
- Fix HTML nesting error in AlertDialog by using div instead of nested p
  tags

This update makes the invoice view properly responsive, removes
misleading marketing claims, and ensures consistent dark mode support
across the entire application.
This commit is contained in:
2025-07-15 02:35:55 -04:00
parent f331136090
commit c9a664869c
71 changed files with 2795 additions and 3043 deletions

View File

@@ -1,100 +1,250 @@
# beenvoice - AI Assistant Guidelines
# beenvoice - AI Assistant Rules
## Project Overview
beenvoice is a modern invoicing application built with the T3 stack (Next.js 15, tRPC, Drizzle/LibSQL, NextAuth.js) and shadcn/ui components. The app allows users to create and manage clients and invoices with a professional, clean interface.
beenvoice is a professional invoicing application built with the T3 stack (Next.js 15, tRPC, Drizzle/LibSQL, NextAuth.js) and shadcn/ui components. This is a business-critical application where reliability, security, and professional user experience are paramount.
## Tech Stack & Architecture
## Core Development Principles
### Core Technologies
- **Frontend**: Next.js 15 with App Router
- **Backend**: tRPC for type-safe API calls
- **Database**: Drizzle ORM with LibSQL (SQLite)
- **Authentication**: NextAuth.js with email/password (Credentials provider)
- **UI**: shadcn/ui components with Tailwind CSS
- **Styling**: Geist font family for professional typography
- **Package Manager**: Bun (with npm fallback)
### 1. Business-First Approach
- **Priority**: Reliability and security over flashy features
- **User Experience**: Professional, clean, and intuitive interface
- **Data Integrity**: Always validate and sanitize user input
- **Error Handling**: Graceful degradation with clear user feedback
### Project Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── api/ # API routes (NextAuth, tRPC)
│ ├── auth/ # Authentication pages
│ ├── clients/ # Client management pages
│ ├── invoices/ # Invoice management pages
│ └── _components/ # Page-specific components
├── components/ # Shared UI components
├── server/ # Server-side code
│ ├── api/ # tRPC routers
│ ├── auth/ # NextAuth configuration
│ └── db/ # Database schema and connection
├── styles/ # Global styles
└── trpc/ # tRPC client configuration
```
### 2. Type Safety & Code Quality
- **TypeScript**: Use strict TypeScript for all new code
- **tRPC**: All API calls must go through tRPC for type safety
- **Validation**: Use Zod schemas for all input validation
- **Error Boundaries**: Implement proper error handling at all levels
## Development Guidelines
## Tech Stack Guidelines
### Code Style & Conventions
- Use TypeScript for all new code
- Follow the existing component patterns with shadcn/ui
- Use the `cn()` utility for conditional className merging
- Implement proper error handling with toast notifications
- Use tRPC for all API calls (no direct fetch calls)
### Frontend (Next.js 15 + App Router)
- Use App Router patterns consistently
- Implement proper loading states and error boundaries
- Follow Next.js 15 best practices for performance
- Use React Server Components where appropriate
### Database Schema
The app uses three main tables:
- **users**: User accounts with email/password authentication
- **clients**: Client information (name, email, phone, address)
- **invoices**: Invoice headers with client relationships
- **invoice_items**: Individual line items with dates, descriptions, hours, rates
### Authentication Flow
- Email/password registration and sign-in
- Password hashing with bcrypt
- Session management via NextAuth.js
- Protected routes require authentication
### UI/UX Principles
- Clean, professional design suitable for business use
- Responsive design that works on all devices
- Consistent spacing and typography using Geist font
- Green color scheme (#16a34a) for branding
- Toast notifications for user feedback
- Modal dialogs for forms and confirmations
### Component Guidelines
- Use shadcn/ui components as the foundation
- Create reusable components in `src/components/`
- Page-specific components go in `src/app/_components/`
- Follow the existing Logo component pattern for branding
### API Development
- All API logic goes through tRPC routers
- Use Zod for input validation
- Implement proper error handling
- Follow the existing router patterns in `src/server/api/routers/`
### Database Operations
### Backend (tRPC + Drizzle)
- All business logic goes through tRPC routers
- Use Drizzle ORM for all database operations
- Follow the existing schema patterns
- Implement proper relationships between tables
- Use transactions for multi-table operations
- Implement proper transactions for multi-table operations
- Follow existing router patterns in `src/server/api/routers/`
## Common Tasks & Patterns
### Database (LibSQL/SQLite)
- Use Drizzle migrations for schema changes
- Implement proper indexes for performance
- Follow existing schema patterns
- Use transactions for data consistency
### Adding New Features
1. Create tRPC router procedures
2. Add validation with Zod schemas
3. Create UI components using shadcn/ui
4. Add proper error handling and user feedback
5. Update navigation if needed
### Authentication (NextAuth.js)
- Email/password authentication with bcrypt hashing
- Proper session management
- Protected routes require authentication
- Follow NextAuth.js security best practices
## Component Architecture
### UI Components (shadcn/ui)
- Use shadcn/ui components as the foundation
- Follow existing component patterns
- Use `cn()` utility for conditional className merging
- Maintain consistent spacing (4px grid system)
### Component Organization
- **Base UI Components**: `src/components/ui/` - Pure, portable shadcn/ui components
- **Project Components**: `src/components/` - Project-specific reusable components
- **Page Components**: `src/app/_components/` - Page-specific components
### UI Component Rules
#### What Belongs in `src/components/ui/` (Portable)
- **Pure shadcn/ui components**: button, input, select, dialog, etc.
- **Generic layout components**: page-layout, card, table
- **Basic form components**: input, textarea, checkbox, switch
- **Navigation components**: breadcrumb, navigation-menu
- **Feedback components**: badge, alert-dialog, toast
- **Data display**: table, skeleton, progress
- **Overlay components**: dialog, sheet, popover, dropdown-menu
#### What Should Move to `src/components/` (Project-Specific)
- **Business logic components**: address-form, status-badge, data-table
- **Domain-specific forms**: client-form, invoice-form, business-form
- **Custom layouts**: page-header, dashboard-breadcrumbs
- **Feature components**: invoice-list, client-list, editable-invoice-items
- **Navigation**: Sidebar, Navbar, navigation
- **Branding**: logo, AddressAutocomplete
#### Immediate Reorganization Needed
**Move from `src/components/ui/` to `src/components/forms/`:**
- `address-form.tsx` - Business-specific address handling
- `file-upload.tsx` - Project-specific file upload logic
**Move from `src/components/ui/` to `src/components/data/`:**
- `data-table.tsx` - Enhanced with business logic
- `status-badge.tsx` - Invoice status specific
- `stats-card.tsx` - Dashboard-specific statistics
**Move from `src/components/ui/` to `src/components/layout/`:**
- `page-layout.tsx` - Project-specific layout patterns
- `quick-action-card.tsx` - Dashboard-specific actions
- `floating-action-bar.tsx` - Project-specific floating actions
**Keep in `src/components/ui/` (Portable):**
- `button.tsx`, `input.tsx`, `select.tsx` - Pure shadcn/ui
- `dialog.tsx`, `sheet.tsx`, `popover.tsx` - Generic overlays
- `table.tsx`, `card.tsx`, `badge.tsx` - Base components
- `calendar.tsx`, `date-picker.tsx` - Generic date components
- `dropdown-menu.tsx`, `navigation-menu.tsx` - Generic navigation
- `skeleton.tsx`, `progress.tsx` - Generic loading states
#### Component Design Principles
- **High Reusability**: Components should accept props for customization
- **Composition over Inheritance**: Use children props and render props
- **Default Values**: Provide sensible defaults for all optional props
- **Type Safety**: Use TypeScript interfaces for all props
- **Accessibility**: Include proper ARIA labels and keyboard navigation
- **Responsive Design**: Mobile-first approach with responsive variants
#### Component Props Pattern
```typescript
interface ComponentProps {
// Required props
title: string;
// Optional props with defaults
variant?: "default" | "success" | "warning" | "error";
size?: "sm" | "md" | "lg";
// Styling props
className?: string;
// Event handlers
onClick?: () => void;
onChange?: (value: string) => void;
// Content
children?: React.ReactNode;
// Accessibility
"aria-label"?: string;
}
```
#### Component Reusability Guidelines
- **Configurable Content**: Use props for text, labels, and content
- **Flexible Styling**: Accept className and style props for customization
- **Variant System**: Use variant props for different visual states
- **Size Variants**: Provide consistent size options (sm, md, lg, xl)
- **Icon Support**: Accept icon props for visual customization
- **Loading States**: Include loading/skeleton variants
- **Error States**: Handle error states gracefully
- **Empty States**: Provide empty state components
#### Component Customization Examples
```typescript
// Good: Highly customizable component
interface DataTableProps<TData> {
columns: ColumnDef<TData>[];
data: TData[];
title?: string;
description?: string;
actions?: React.ReactNode;
searchKey?: string;
searchPlaceholder?: string;
showPagination?: boolean;
pageSize?: number;
className?: string;
emptyState?: React.ReactNode;
loading?: boolean;
}
// Good: Flexible form component
interface FormFieldProps {
label: string;
name: string;
type?: "text" | "email" | "password" | "number";
placeholder?: string;
required?: boolean;
error?: string;
className?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
disabled?: boolean;
onChange?: (value: string) => void;
}
```
#### Component File Structure
```
src/components/
├── ui/ # Portable base components
│ ├── button.tsx # Pure shadcn/ui components
│ ├── input.tsx
│ └── ...
├── forms/ # Project-specific forms
│ ├── address-form.tsx
│ ├── client-form.tsx
│ └── invoice-form.tsx
├── layout/ # Layout components
│ ├── page-header.tsx
│ ├── sidebar.tsx
│ └── navbar.tsx
├── data/ # Data display components
│ ├── data-table.tsx
│ ├── status-badge.tsx
│ └── invoice-list.tsx
├── navigation/ # Navigation components
│ ├── breadcrumbs.tsx
│ └── navigation.tsx
└── branding/ # Brand-specific components
├── logo.tsx
└── address-autocomplete.tsx
```
### Styling Guidelines
- Use Tailwind CSS classes
- Follow the existing color scheme
- Use Geist font family
- Maintain consistent spacing (4px grid system)
- Use the existing component patterns
- **Primary Color**: Green (#16a34a) for branding
- **Font**: Geist font family for professional typography
- **Tailwind CSS**: Use utility classes consistently
- **Responsive Design**: Mobile-first approach
## Business Logic Patterns
### Invoice Management
- **Invoice Creation**: Multi-step process with validation
- **Line Items**: Flexible pricing with custom rates
- **Status Tracking**: draft → sent → paid/overdue
- **PDF Generation**: Professional invoice formatting
### Client Management
- **Contact Information**: Complete address and contact details
- **Search & Filter**: Efficient client lookup
- **Data Validation**: Proper email and phone validation
### Business Profile
- **Default Business**: One default business per user
- **Logo Support**: Professional branding
- **Tax Information**: Business tax details
## API Development Rules
### tRPC Router Patterns
```typescript
// Follow this pattern for all routers
export const exampleRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({ /* validation schema */ }))
.mutation(async ({ ctx, input }) => {
// Business logic here
}),
list: protectedProcedure
.input(z.object({ /* pagination/filtering */ }))
.query(async ({ ctx, input }) => {
// Query logic here
}),
});
```
### Error Handling
- Use toast notifications for user feedback
@@ -102,28 +252,177 @@ The app uses three main tables:
- Handle API errors gracefully
- Provide clear error messages
## File Naming Conventions
- Components: PascalCase (e.g., `ClientList.tsx`)
- Pages: kebab-case (e.g., `new-client.tsx`)
- Utilities: camelCase (e.g., `formatCurrency.ts`)
- Constants: UPPER_SNAKE_CASE
### Security
- Always validate user input with Zod
- Check user permissions for all operations
- Sanitize data before database operations
- Use proper authentication checks
## Testing Considerations
- Ensure all forms have proper validation
- Test responsive design on different screen sizes
- Verify authentication flows work correctly
- Test database operations with proper error handling
## Database Schema Rules
### Table Structure
- **UUID Primary Keys**: Use `crypto.randomUUID()` for all IDs
- **Timestamps**: Include `createdAt` and `updatedAt` fields
- **User Relations**: All business data linked to users
- **Indexes**: Proper indexing for performance
### Relationships
- **Users → Clients**: One-to-many
- **Users → Businesses**: One-to-many
- **Users → Invoices**: One-to-many
- **Clients → Invoices**: One-to-many
- **Businesses → Invoices**: One-to-many
- **Invoices → Invoice Items**: One-to-many
## File Naming Conventions
### Components & Pages
- **Components**: PascalCase (e.g., `ClientList.tsx`)
- **Pages**: kebab-case (e.g., `new-client.tsx`)
- **Layouts**: `layout.tsx` (Next.js convention)
- **API Routes**: `route.ts` (Next.js convention)
### Utilities & Helpers
- **Utilities**: camelCase (e.g., `formatCurrency.ts`)
- **Constants**: UPPER_SNAKE_CASE
- **Types**: PascalCase (e.g., `InvoiceStatus`)
## Development Workflow
### Adding New Features
1. **Database Schema**: Update schema with migrations
2. **tRPC Router**: Add procedures with validation
3. **UI Components**: Create components using shadcn/ui
4. **Pages**: Implement pages with proper routing
5. **Testing**: Verify functionality and error handling
### Code Quality
- **ESLint**: Follow existing linting rules
- **Prettier**: Consistent code formatting
- **TypeScript**: Strict type checking
- **Performance**: Optimize database queries and React components
## Business Logic Specifics
### Invoice Calculations
- **Subtotal**: Sum of all line items
- **Tax**: Apply tax rate to subtotal
- **Total**: Subtotal + tax
- **Currency**: Handle decimal precision properly
### Status Management
- **Draft**: Initial state, editable
- **Sent**: Invoice sent to client
- **Paid**: Payment received
- **Overdue**: Past due date
### Data Validation
- **Email**: Proper email format validation
- **Phone**: International phone number support
- **Address**: Complete address validation
- **Currency**: Proper decimal handling
## Performance Guidelines
- Use Next.js Image component for images
### Database Optimization
- Use proper indexes for frequently queried fields
- Implement pagination for large datasets
- Use transactions for data consistency
- Optimize query patterns
### Frontend Performance
- Use React.memo for expensive components
- Implement proper loading states
- Optimize database queries
- Use React.memo for expensive components when needed
- Optimize bundle size
- Use Next.js Image component for images
### Caching Strategy
- Use React Query for client-side caching
- Implement proper cache invalidation
- Use Next.js caching where appropriate
## Security Considerations
- Always validate user input
- Use proper authentication checks
- Sanitize data before database operations
- Follow NextAuth.js security best practices
Remember: This is a business application, so prioritize reliability, security, and professional user experience over flashy features.
### Authentication & Authorization
- All routes require proper authentication
- Check user ownership for all operations
- Implement proper session management
- Use secure password hashing
### Data Protection
- Validate all user inputs
- Sanitize data before database operations
- Implement proper error handling
- Use HTTPS in production
### Business Data Security
- User data isolation
- Proper access controls
- Audit trails for sensitive operations
- Secure API endpoints
## Testing & Quality Assurance
### Manual Testing Checklist
- [ ] Authentication flows work correctly
- [ ] Form validation provides clear feedback
- [ ] Responsive design on all screen sizes
- [ ] Database operations handle errors gracefully
- [ ] PDF generation works correctly
- [ ] Navigation and routing function properly
### Code Review Guidelines
- Check for proper error handling
- Verify type safety
- Ensure consistent styling
- Review security implications
- Test business logic accuracy
## Deployment & Production
### Environment Configuration
- Use proper environment variables
- Secure database connections
- Configure NextAuth.js properly
- Set up proper logging
### Database Management
- Use migrations for schema changes
- Backup data regularly
- Monitor database performance
- Handle database errors gracefully
## Common Patterns & Anti-Patterns
### ✅ Do's
- Use tRPC for all API calls
- Implement proper loading states
- Use toast notifications for feedback
- Follow existing component patterns
- Validate all user inputs
- Use proper TypeScript types
### ❌ Don'ts
- Don't use direct fetch calls
- Don't skip input validation
- Don't ignore error handling
- Don't hardcode business logic
- Don't use any types unnecessarily
- Don't skip proper authentication checks
## Emergency Procedures
### Critical Issues
- **Data Loss**: Immediate database backup
- **Security Breach**: Rotate all secrets
- **Performance Issues**: Database query optimization
- **User Complaints**: Prioritize user experience fixes
### Rollback Strategy
- Keep database migrations reversible
- Maintain version control for all changes
- Test rollback procedures regularly
- Document emergency procedures
## Remember
This is a business application where reliability, security, and professional user experience are critical. Every decision should prioritize these values over development convenience or flashy features.

View File

@@ -1,303 +0,0 @@
# Dark Mode Implementation Guide
## Overview
BeenVoice implements a **media query-based dark mode** system that automatically adapts to the user's system preferences. This approach provides a seamless experience without requiring manual theme switching controls.
## Implementation Approach
### Media Query-Based vs Class-Based
We chose **media query-based** dark mode (`@media (prefers-color-scheme: dark)`) over class-based dark mode for the following reasons:
- **Automatic System Integration**: Respects user's OS/browser theme preference
- **No JavaScript Required**: Pure CSS solution with better performance
- **Better Accessibility**: Follows system accessibility settings
- **Seamless Experience**: No flash of incorrect theme on page load
- **Reduced Complexity**: No need for theme toggle components or state management
## Configuration Changes
### 1. Tailwind CSS Configuration
**File:** `tailwind.config.ts`
```typescript
export default {
darkMode: "media", // Changed from "class" to "media"
// ... rest of config
} satisfies Config;
```
### 2. Global CSS Updates
**File:** `src/styles/globals.css`
Key changes made:
- Replaced `.dark { }` class selector with `@media (prefers-color-scheme: dark) { :root { } }`
- Maintained all existing CSS custom properties
- Updated dark mode color definitions to use media queries
```css
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
/* ... all other dark mode variables */
}
}
```
## Component Updates
### Core Layout Components
#### 1. Root Layout (`src/app/layout.tsx`)
- Updated background gradients with dark mode variants
- Improved layering structure for background effects
- Added proper z-index management
#### 2. Landing Page (`src/app/page.tsx`)
- Comprehensive dark mode classes for all sections
- Updated text colors, backgrounds, and hover states
- Maintained brand consistency with green color scheme
#### 3. Authentication Pages
- **Sign In Page** (`src/app/auth/signin/page.tsx`)
- **Register Page** (`src/app/auth/register/page.tsx`)
Both pages updated with:
- Dark background gradients
- Card component dark backgrounds
- Input field icon colors
- Text color variations
- Link hover states
### Navigation Components
#### 1. Navbar (`src/components/Navbar.tsx`)
- Glass morphism effect with dark background support
- Button variant adaptations
- Text color adjustments for user information
#### 2. Sidebar (`src/components/Sidebar.tsx`)
- Dark background with transparency
- Navigation link states (active/hover)
- Border color adaptations
- Icon and text color updates
#### 3. Mobile Sidebar (`src/components/SidebarTrigger.tsx`)
- Sheet component dark styling
- Navigation link consistency with desktop sidebar
- Border and background adaptations
### Dashboard Components
#### 1. Dashboard Page (`src/app/dashboard/page.tsx`)
- Welcome text color adjustments
- Maintained gradient text effects
#### 2. Universal Table (`src/components/ui/universal-table.tsx`)
- Comprehensive table styling updates
- Header background and text colors
- Cell content color adaptations
- Status badge color schemes
- Pagination control styling
- Grid view card backgrounds
## Color System
### Text Colors
- **Primary Text**: `text-gray-900 dark:text-white`
- **Secondary Text**: `text-gray-700 dark:text-gray-300`
- **Muted Text**: `text-gray-500 dark:text-gray-400`
- **Icon Colors**: `text-gray-400 dark:text-gray-500`
### Background Colors
- **Card Backgrounds**: `dark:bg-gray-800`
- **Hover States**: `hover:bg-gray-100 dark:hover:bg-gray-800`
- **Border Colors**: `border-gray-200 dark:border-gray-700`
- **Glass Effects**: `bg-white/60 dark:bg-gray-900/60`
### Brand Colors
Maintained consistent green brand colors with dark mode adaptations:
- **Primary Green**: `text-green-600 dark:text-green-400`
- **Emerald Accents**: `bg-emerald-100 dark:bg-emerald-900/30`
## Testing the Implementation
### Manual Testing
1. **System Theme Toggle**:
- Change your OS dark/light mode setting
- Verify automatic theme switching
- Check for smooth transitions
2. **Page Coverage**:
- Landing page
- Authentication pages (sign in/register)
- Dashboard and navigation
- All table views and components
3. **Component Testing**:
- Form elements (inputs, buttons, selects)
- Cards and containers
- Navigation states (hover, active)
- Text readability in both modes
### Browser Developer Tools
1. **Media Query Testing**:
```css
/* In DevTools Console */
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';
```
2. **Emulation**:
- Chrome DevTools > Rendering > Emulate CSS prefers-color-scheme
- Firefox DevTools > Settings > Dark theme simulation
### Test Component
A comprehensive test component is available at `src/components/dark-mode-test.tsx` that displays:
- Color variations
- Button states
- Form elements
- Status indicators
- Background patterns
- Icon colors
## Best Practices for Future Development
### 1. Color Class Patterns
Always pair light mode colors with dark mode variants:
```tsx
// ✅ Good
<div className="text-gray-900 dark:text-white bg-white dark:bg-gray-800">
// ❌ Avoid
<div className="text-gray-900 bg-white">
```
### 2. Common Patterns
**Text Colors:**
```tsx
// Primary text
className="text-gray-900 dark:text-white"
// Secondary text
className="text-gray-700 dark:text-gray-300"
// Muted text
className="text-gray-500 dark:text-gray-400"
```
**Backgrounds:**
```tsx
// Card backgrounds
className="bg-white dark:bg-gray-800"
// Hover states
className="hover:bg-gray-100 dark:hover:bg-gray-800"
// Borders
className="border-gray-200 dark:border-gray-700"
```
**Interactive Elements:**
```tsx
// Links
className="text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
// Active states
className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
```
### 3. Component Development Guidelines
1. **Always test both modes** during development
2. **Use semantic color tokens** from the design system when possible
3. **Maintain sufficient contrast** for accessibility
4. **Consider glass morphism effects** with appropriate alpha values
5. **Test with real content** to ensure readability
### 4. shadcn/ui Components
Most shadcn/ui components already include dark mode support:
- Button variants adapt automatically
- Input fields use CSS custom properties
- Card components respond to theme changes
For custom components, follow the established patterns.
## Troubleshooting
### Common Issues
1. **Colors Not Switching**:
- Verify `darkMode: "media"` in `tailwind.config.ts`
- Check CSS custom properties are properly defined
- Ensure no conflicting class-based dark mode styles
2. **Flash of Incorrect Theme**:
- Should not occur with media query approach
- If present, check for JavaScript theme switching code
3. **Incomplete Styling**:
- Search for hardcoded colors: `text-gray-XXX` without `dark:` variants
- Use component test page to verify all elements
4. **Performance Issues**:
- Media query approach should have no performance impact
- CSS variables resolve efficiently
### Debugging Tools
1. **CSS Custom Properties Inspector**:
```javascript
// In DevTools Console
getComputedStyle(document.documentElement).getPropertyValue('--background')
```
2. **Media Query Detection**:
```javascript
// Check current preference
window.matchMedia('(prefers-color-scheme: dark)').matches
```
## Browser Support
The media query-based approach is supported in:
- Chrome 76+
- Firefox 67+
- Safari 12.1+
- Edge 79+
For older browsers, the light theme serves as a fallback.
## Maintenance
### Regular Checks
1. **New Component Integration**: Ensure new components follow dark mode patterns
2. **Third-party Components**: Verify external components adapt to theme
3. **Asset Updates**: Check images/icons work in both modes
4. **Performance Monitoring**: Ensure no CSS bloat from unused dark mode classes
### Updates and Migration
If future requirements need class-based dark mode:
1. Update `tailwind.config.ts` to `darkMode: "class"`
2. Add theme toggle component
3. Implement theme persistence
4. Update CSS to use `.dark` selector instead of media queries
## Conclusion
The media query-based dark mode implementation provides a robust, performant, and user-friendly theming solution that automatically adapts to user preferences while maintaining design consistency and accessibility standards.

View File

@@ -1 +0,0 @@

View File

@@ -1,49 +0,0 @@
# Invoice PDF Requirements
## Purpose
Generate professional, print-ready invoice PDFs that represent the beenvoice brand and provide a good user experience.
## Core Requirements
### Visual Design
- Professional appearance suitable for business use
- Clean, modern layout that matches the app's design language
- Consistent branding with the beenvoice logo and colors
- Print-friendly design with good contrast and readability
### Content Structure
- Header with logo, invoice title, number, and status
- Client information and invoice details in organized sections
- Itemized table with all invoice line items
- Clear total amount display
- Professional footer with branding
### Typography & Layout
- Readable fonts that work across all systems
- Proper hierarchy with clear headings and body text
- Consistent spacing and alignment throughout
- Responsive layout that works on different page sizes
### Technical Requirements
- Generate high-quality PDFs suitable for printing
- Support multi-page documents when content is long
- Ensure consistent rendering across different browsers
- Fast generation without blocking the UI
- Proper page numbering and navigation
### User Experience
- PDFs should be immediately recognizable as beenvoice invoices
- Content should be well-organized and easy to scan
- Professional appearance that builds trust with clients
- Clear call-to-action and payment information
### Brand Consistency
- Use the same logo and colors as the main application
- Maintain the same visual language and design principles
- Ensure the PDF feels like part of the beenvoice ecosystem
## Quality Standards
- Professional enough for client-facing business use
- Consistent with modern invoice design standards
- Accessible and readable for all users
- Print-ready with proper margins and formatting

View File

@@ -1,742 +0,0 @@
-- beenvoice Data Export
-- Generated: 2025-07-12T05:58:17.481Z
-- Run these INSERT statements in your Turso database
-- Data for beenvoice_user (2 records)
INSERT INTO beenvoice_user VALUES('1ca66210-7d70-43d1-b01b-07004f566ac8','Sean O''Connor','sean@soconnor.dev','$2b$12$ntXp5nKRyNyf9HzQFaodVO/yjKHjCW6lG0.MiIH0U74o4y15Jz0Cu',1752122289,NULL);
INSERT INTO beenvoice_user VALUES('08305460-ee86-430b-aa8b-a5280b4a1d5b','Test User','test@example.com','$2b$12$Qh7kl3I0poJCBlitIm9HeumOPCh0zRdgl161KrCyxTNeVi979Lb7C',1752122648,NULL);
-- Data for beenvoice_client (3 records)
INSERT INTO beenvoice_client VALUES('81edd8a8-c5c7-4f16-ab71-0efedbe3aff7','Hoosier Tire of Calverton','ar@riverheadraceway.com','(631) 842-7223','1797 Old Country Rd','','Riverhead','NY','11901','','1ca66210-7d70-43d1-b01b-07004f566ac8',1752129038,1752129178);
INSERT INTO beenvoice_client VALUES('1c17bccd-3bc6-42c2-a500-68728a2a9d25','Riverhead Raceway','ar@riverheadraceway.com','(631) 842-7223','1797 Old Country Rd','','Riverhead','NY','11901','United States','1ca66210-7d70-43d1-b01b-07004f566ac8',1752129251,1752129251);
INSERT INTO beenvoice_client VALUES('8c24c053-9f84-49be-95e3-30fe9cdcdeef','TDE, Inc.','tvtimd@aol.com','(413) 575-6125','116 Dowd Ct','','Ludlow','MA','01056','United States','1ca66210-7d70-43d1-b01b-07004f566ac8',1752129474,1752129474);
-- Data for beenvoice_business (1 records)
INSERT INTO beenvoice_business VALUES('20ef93d6-b1c4-4f9a-b1c1-e62423770f6b','Sean O''Connor','sean.oconnor@riverheadraceway.com','(631) 601-6555','14 Washington Avenue','','Miller Place','NY','11764','United States','https://soconnor.dev','','',1,'1ca66210-7d70-43d1-b01b-07004f566ac8',1752277286,1752277286);
-- Data for beenvoice_invoice (380 records)
INSERT INTO beenvoice_invoice_item VALUES('9b237b0e-d47e-47d3-9351-777d10c84d38','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64731153600,'Virtual',1.5,20.0,30.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('8fb85a95-50f9-4375-86d2-5e0e334d87ce','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64731326400,'In person',3.0,20.0,60.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('d9f841ae-4c70-4b3d-ba6a-befec3e07693','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64732363200,'In person',2.0,20.0,40.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('fd91ea66-4c98-468d-a1ae-1d6715c028c2','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64732536000,'In person',4.5,20.0,90.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('bb1b3ccc-35be-47b9-a328-386d7fdc0260','61c3d28c-5031-4372-86e3-5bf895411046',64733054400,'In person',2.5,20.0,50.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('33de41fb-3117-4bef-8ced-b9955538f920','61c3d28c-5031-4372-86e3-5bf895411046',64733140800,'In person',5.5,20.0,110.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('22f8db2c-4d80-4847-8927-7fcce399627e','61c3d28c-5031-4372-86e3-5bf895411046',64733572800,'In person',3.0,20.0,60.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('52d4126f-3e1b-4f11-a1cd-c14f64ef8785','61c3d28c-5031-4372-86e3-5bf895411046',64733745600,'Race day (flat rate)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('b9588fcb-2081-44f4-a167-2b51567d89a1','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1621051200,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('fd024a5f-1bf3-4a08-9fb1-fd39502158eb','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1621656000,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('63e0e171-d1f9-43a7-a465-d883b4996b53','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1622865600,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('af8b2c9d-147b-49b4-b0a7-0a98ba63abee','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1620619200,'Fix routers',3.0,20.0,60.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('f9f4712d-9096-4322-978f-3fdff9591939','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1620792000,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('dc0dc83c-093a-42c1-9c8e-b658f5cac7ef','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1621396800,'Race Day (Fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('c1d379b1-70ea-44c4-a3cd-d4e1f1510722','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1622520000,'RDP Login Configuration',2.5,20.0,50.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('1212495d-3d81-47ed-ad57-2f938330a95b','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1338696000,'Virtual Database Install/Setup',5.0,20.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('dfd60b61-908c-4a8e-b768-c471cbf1699a','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1623297600,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('c79fc31f-2abb-4b4e-968b-8ced90992bfb','4fb5d8be-2588-4187-955d-e7643b08619f',1627617600,'Office Internet/3Play Configuration',4.0,20.0,80.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('286c3631-0a36-4177-83e2-e041d3e5e198','4fb5d8be-2588-4187-955d-e7643b08619f',1627704000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('a0599f94-dcbb-4ff7-8f69-f685b200d702','4fb5d8be-2588-4187-955d-e7643b08619f',1628308800,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('96f1bee1-1117-4fb8-a40a-4fcd485d6528','f48104da-1baa-4a70-9d0c-c03f4017f60d',1628740800,'Stream Deck/Tower Server',2.5,20.0,50.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('41fcea00-259c-433c-8744-1da4297ee261','f48104da-1baa-4a70-9d0c-c03f4017f60d',1628913600,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('a5a677ea-1c26-4c93-bee5-4e7193d8fc54','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629432000,'Office Server Ransomware/Data Recovery',5.0,20.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('fc9e7932-aae0-4611-8dfd-439632e02efe','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629518400,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('14d8f2e4-79a1-4f52-80cb-495422c2ff6c','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629864000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('848765c1-2f93-4fe0-bd54-83a8ed6e028b','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1630728000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('28b59943-9beb-4c64-bf94-f10729ef55e9','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631332800,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('ef1b5cc8-046e-4720-9126-365bf2011cef','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631419200,'Office Server Data Migration (Online)',2.0,20.0,40.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('07d42569-5d78-4ddc-9146-07c68df081f0','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631937600,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('f87f4371-4d88-461b-9e20-218841842abd','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1635739200,'IT Move server/Vmix/Backups',2.0,20.0,40.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('fb6b4cf4-b569-42ac-ba14-53e242d07560','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1635825600,'Prep In Car Cameras',3.0,20.0,60.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('f31d6dab-9af3-476c-a272-6e53c3e81a51','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1636520400,'Race Day,Islip 300',1.0,100.0,100.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('0a800d16-bf03-4139-93f6-872a455fbd57','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1649390400,'Hoosier Tire Scanning',3.0,20.0,60.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('91c2086d-590a-45ff-8857-006964144c6c','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1649736000,'SSD Migration/Data Backup',4.0,20.0,80.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('d705c999-1112-4215-97c8-81888281a27d','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1650513600,'Roster Numbers/Data Migration',5.5,20.0,110.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('1cf32daa-b16e-47a9-8d17-3bb65e8bf654','a0da2a05-5681-46fd-b988-235ec24971e2',1651636800,'Laptop setup/Facebook stream',5.0,20.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('87cee56a-7582-4015-9183-7b917b685b7a','a0da2a05-5681-46fd-b988-235ec24971e2',1652500800,'Race Day',1.0,100.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('adf5caac-3381-4811-aa9a-fe64c6c0ad20','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652500800,'Tire Sales (Hoosier)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('d278998a-ed4e-47bd-8915-35124d8bc27f','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652846400,'Raceway CMS Development',6.0,20.0,120.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('694aaa24-b883-4aa1-b365-3e3ded6e9c4f','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652932800,'Raceway CMS Development',5.0,20.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9f5571af-e79e-4254-a370-deb25f16f06c','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653019200,'Raceway CMS Development',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('d9f1fcca-a6f1-4f4e-a6ba-52ea102db90a','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653105600,'Race day (RR)',6.0,20.0,120.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('ebb93ccc-4a9d-4d6f-8584-f044377fdc00','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653105600,'Tire Sales (Hoosier)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('a4f27be7-68ec-492a-b127-21fa207bde52','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653192000,'Raceway CMS Development',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9749ad17-0e9b-4682-8011-aee73425354b','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653278400,'Raceway CMS Development',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('29fadf5f-a919-420c-a4a7-778d62b770f9','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653364800,'Raceway CMS Development',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('b7f57cc5-ecea-49e3-bb42-15e90dfba1df','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653537600,'Raceway CMS Development',1.0,20.0,20.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('a8380c9d-0444-4afe-b820-9597a871a903','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653624000,'Generate points tables on site/tire LAN',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('fd72e334-4e6a-4462-82a5-cc5a8d3ecda0','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654056000,'Press Release Publish',1.0,20.0,20.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('7fc11bdd-b740-4c2a-9cf1-2c3bab092f77','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654315200,'Race Day (RR)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('23fbcc77-d0d2-4d0e-90d5-e2f9cab790f7','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654488000,'PR Archive Integration/Points Update',2.0,20.0,40.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('70be501b-a496-4f40-aebc-a4521fbcf4ba','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654574400,'Press Release Website Deployment',2.0,20.0,40.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9bf35628-6046-44e2-a24f-681ea5bf7bb9','8704d2fe-8972-4dae-8062-2f5b81e14493',1654747200,'Raceway CMS Development',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('abea397c-2ea5-4788-9560-42ea0d508bce','8704d2fe-8972-4dae-8062-2f5b81e14493',1654833600,'Raceway CMS Development',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('99590ee9-7e6a-40ec-8925-b135457ba01e','8704d2fe-8972-4dae-8062-2f5b81e14493',1655092800,'TRMM Installation/Script Writing',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('e1f534be-9fe8-42f1-8ef4-bc4073d8ce2b','8704d2fe-8972-4dae-8062-2f5b81e14493',1655265600,'PC Updates',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('7e619cdf-af99-4c58-be0e-227324710e4e','8704d2fe-8972-4dae-8062-2f5b81e14493',1655352000,'3Play Remote Access Setup',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('7fd71054-7626-4f53-94e9-5fc4006ca3c4','8704d2fe-8972-4dae-8062-2f5b81e14493',1655524800,'Race Day',8.0,20.0,160.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c9d74e45-d270-45b4-9332-25db44c9d6d1','8704d2fe-8972-4dae-8062-2f5b81e14493',1655697600,'Move and reassign printer',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('ae2ef12b-43b2-454e-90bf-d8b150f89278','8704d2fe-8972-4dae-8062-2f5b81e14493',1655870400,'Website updates/PR Logic',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('f96d139f-7f40-4a25-89cc-05510c782a7d','8704d2fe-8972-4dae-8062-2f5b81e14493',1656302400,'Website updates',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('e4c9c5ac-ee0d-490f-9f84-c542ad4b7c5c','8704d2fe-8972-4dae-8062-2f5b81e14493',1656475200,'Website updates/schedule/press-release',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('fb35940e-4199-4de8-a163-5ffac86ab0c4','babfc847-b37d-44f2-91a9-4251691c11b4',1658376000,'Server updates and TMM',5.0,20.0,100.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('8f4a5ca2-88d9-403e-9098-6b398d4be218','babfc847-b37d-44f2-91a9-4251691c11b4',1658548800,'Race Day (RR)',9.0,20.0,180.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('9107c846-b7b9-4d37-aecf-8b7cbc6cfc70','babfc847-b37d-44f2-91a9-4251691c11b4',1658721600,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('51e881b1-0e7f-4bcd-87da-3512e2345337','babfc847-b37d-44f2-91a9-4251691c11b4',1658808000,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('707b9108-81ea-4af6-aa2f-0de09220a1a8','babfc847-b37d-44f2-91a9-4251691c11b4',1658894400,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('9d4c904c-2421-442f-9b45-09a330de83a4','babfc847-b37d-44f2-91a9-4251691c11b4',1658980800,'CMS Development (in person)',5.0,20.0,100.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('5685ea85-1190-45b5-bc0b-65d3a0ae37f5','babfc847-b37d-44f2-91a9-4251691c11b4',1659153600,'Race Day (Hoosier)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1d14d0de-642f-4266-a466-30ba7773b55f','babfc847-b37d-44f2-91a9-4251691c11b4',1659153600,'Race Day (RR) / Drone photography',6.0,20.0,120.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('93dfc1f6-e3d7-4c5a-8684-32534458bae9','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1660017600,'Update points, change prices, fix pdf display',1.0,20.0,20.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('24f10b26-5ccb-4217-ae89-11d601b16f67','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1660104000,'Add Penalty Reports to CMS',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('f0f74076-daed-4e92-9693-ede280cc3e19','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1672203600,'Server drive replacement/data recovery',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('40e280f9-1a60-4765-9d3c-bbd6f7546e0a','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643605200,'Add PR support to CMS',4.5,20.0,90.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c3160122-ac8c-4e1e-9f12-be70dae50d38','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643691600,'Deploy PR update to CMS backend',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1c23838c-134e-486a-8e94-7d2d085ce4b2','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643778000,'Update database schema to support PR',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c6c1d55b-0895-4e4c-9b48-2de18dd4b3a8','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643778000,'Patch riverheadraceway.com frontend for PR',2.5,20.0,50.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1ce0b765-4330-454e-b339-679d3a61560c','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644123600,'Begin new schema for schedule upload',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('41f3b4a2-d0ac-4813-8c97-353151735140','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644642000,'Prototype rules upload page',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('296f2767-f2a1-48ee-afdd-7d9e5a5d4373','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644814800,'Rules CMS page backend dev',1.0,20.0,20.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('6df444f9-9013-4ba4-889f-288687bf40cd','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679025600,'Website fixes, Orbits Suite Update (5.9)',4.0,20.0,80.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('b8b7d422-8eb4-4550-8b8c-75d1eebb606c','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679284800,'Backblaze B2 Backup setup for VMs/web',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('0a871404-dd23-47ae-9b53-4db1762424db','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679371200,'Install and provision Active Directory SRV',4.5,20.0,90.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('e6ae1b2c-e842-431d-b2d1-fbe46f0d29d5','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679457600,'Update BackBlaze configurations',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f540b78f-10a7-4f10-9409-10f54eff831e','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679457600,'Website edits',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('7dd05c87-6a3e-40e6-836a-63dd7e22d52c','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679544000,'Remove policies from website',0.5,20.0,10.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('5dd99069-2388-4daa-b304-a5e6f000bbaa','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679544000,'Add dynamic roster to website',3.5,20.0,70.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('64f37c00-5f5f-49d3-85bc-786d083abc01','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679630400,'Update handicapping rules, modify reserved',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f7845e1a-cfaf-4b97-9404-985c578cd35d','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679889600,'Update CMOD rules, separate bandos',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('1967e5b2-98ae-493b-ba34-b28c81ebeed9','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681272000,'Separate/ configure user accounts for FM',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('623d2cee-7c09-4626-bff7-16b4af75a3ac','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681704000,'Generate and email RDP deployments',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('9a5cac21-e2f8-4028-b90d-2f1d1701abb6','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681876800,'Generate roster CSV and convert to FM',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('dda3b050-8ad8-45fb-bee5-48bc2e94c469','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1682395200,'Troubleshoot FM access',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('2e562992-b42e-4d1d-99eb-ff354b2194d8','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1682568000,'Generate login certificates/install FM server',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8542b97d-a5e5-4a95-b143-9677c9ca2c09','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1683000000,'Reset RDP cache on Vmix PC/initialize',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('54ba18de-ae79-4f36-a5f7-5e112e7033fe','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1683086400,'Unify user accounts on AD for FM',2.5,20.0,50.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6446d0af-4267-42fb-b929-18705adf748a','0b057a65-fe7d-4495-8756-4dd61f6895e1',1683777600,'On-site- printer and system update/config',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('967b4092-caf5-4fe9-94ae-9ad05d021abd','0b057a65-fe7d-4495-8756-4dd61f6895e1',1683950400,'Race day',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d13204e9-14e8-4cf6-af8d-0d554f865897','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684123200,'FM Maintenance/Web development',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('e3e9ce1b-ed84-47e3-822f-f844b7aa0484','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684209600,'PointsSplitter Script (Remote)',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('eb535a1b-315f-4457-b742-72d01419b2cd','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684296000,'vMix,New ticker and sponsors',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8c318cea-7d7d-4ec5-a6df-63b46e1d36be','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684382400,'Web Development (Remote)',4.0,20.0,80.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6def29d4-4511-4705-b963-29717f881a7a','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684468800,'MyRacePass/Website',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('2a3a5028-d561-43ac-af77-2a2af562b145','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684641600,'Race day',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d4cba322-8a53-4f72-9e10-16388bbc5e51','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684728000,'MyRacePass Data/FM Server/3Play',4.5,20.0,90.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d2180f5c-685d-4f5e-8a03-b8f6804bbf31','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684814400,'FileMaker Troubleshooting/Maintenance',2.5,20.0,50.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('7feefdb6-1a66-439c-8013-a354d7af4284','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684900800,'New graphics suite',5.5,20.0,110.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d23b7924-9acc-48c4-9d09-067b6f12c0b6','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685073600,'TV Lineups program',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6bd45327-8f01-4dab-8a92-9b76363ce2d3','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685160000,'Race Day',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('77d6c533-8d6b-437f-b3bb-7f51dd8f8e5b','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685505600,'PC Maintenance',5.5,20.0,110.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d83da575-2e45-4dd6-bf39-4f1b553a3d4f','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685592000,'Web Development/CMS backend update',6.0,20.0,120.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('9dfc99e1-87b2-4487-90e2-3d7410bf771f','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685592000,'Equipment Purchase - Black Box',1.0,170.0,170.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f0ed5f32-fa79-43d8-bbb1-02859b9a9f7d','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685678400,'TV setup and wiring',3.0,20.0,60.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8e754e3f-eee4-4af7-93c4-1238f32d572c','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685764800,'Race Day',3.0,20.0,60.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('62ea502c-a52a-462d-93ed-8deb5b8b97af','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1685937600,'Website updates, capture card fix',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('97e3d7c5-a2bc-484b-90ee-8883fafd6842','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686024000,'Web dev',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c2fc47ee-9c96-4b66-9a87-fe52054ab6e7','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686110400,'Website work, Itinerary/Roster fixes',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('08aab334-1ef7-470e-b02b-99b8991cbf78','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686283200,'Quickbooks reinstall/drive copy (on-site)',1.0,20.0,20.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('17957359-ecdd-49be-8a23-257c7bc45e81','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686283200,'Web development',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('bc1cb3aa-3d66-4b6d-9089-6bdda101503c','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686369600,'Race Day',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('3489a122-5983-461f-bf54-edc0df82a89d','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1687492800,'Reset passwords, hide enduro points',1.5,20.0,30.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('afc213f9-5738-48a2-9be2-91264ee2fd70','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688356800,'On-site website work',6.5,20.0,130.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('5c7c9b42-8da1-4cfe-a683-b2175588d4a0','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688443200,'Remote website work',3.5,20.0,70.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ee858f44-df11-4754-a105-418a0c392f5a','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688443200,'Microsoft Office 2019 ProPlus',1.0,30.0,30.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ddf90623-e1c7-4d77-b215-20e3bdcf057c','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688529600,'On-site website work',6.0,20.0,120.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('62185930-638a-4de1-80f4-cb594af09848','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688702400,'On-site website work',9.0,20.0,180.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c4a6df10-ff6b-475f-9614-3ab87bc891dc','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688788800,'Race Day',9.0,20.0,180.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('6c7cb646-294c-4279-bd78-986b84b99c01','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688875200,'Website work',3.0,20.0,60.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b4575be6-65d8-435d-974f-e3a741500ba4','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1688961600,'On-site website work',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('f2282616-a3f4-4920-9d12-c89251d67468','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689048000,'Remote website work',6.5,20.0,130.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ede3c1c2-d80e-489a-945d-a61e24e15f1f','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689134400,'Remote website work',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('af24fb33-cde7-4bb8-a0ba-b81a9fb6222c','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689220800,'On-site computer work',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('1e16ad5f-a961-46ed-a58c-4423a830839c','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689220800,'Remote website work',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b575b193-ce8e-415c-8689-6a8fac8e7a1f','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689307200,'On-site computer/website',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('2f4fd87b-4a2e-4ddb-88c3-770a36bf5640','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689393600,'Race Day',6.0,20.0,120.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b2525223-cf5e-4a2e-a07c-ba3972f51409','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689393600,'Acer SB220Q',1.0,80.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c859a866-6487-432d-ad05-cf6bc732c6c6','a722008f-f269-4018-b755-b25cd2c5471a',1658030400,'Website (off-site)',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('284084d7-cb14-40db-8017-99aa6182741f','a722008f-f269-4018-b755-b25cd2c5471a',1658116800,'Website (on-site)',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ea1457ef-79a9-4d36-95e9-98667ab57de4','a722008f-f269-4018-b755-b25cd2c5471a',1658203200,'Website (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('64d111a0-6371-44d5-994d-afd5e47491ca','a722008f-f269-4018-b755-b25cd2c5471a',1658289600,'Move ThinkCentre/Tires',7.0,20.0,140.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('a853c17d-bc3e-45a4-88d0-48ca01631e88','a722008f-f269-4018-b755-b25cd2c5471a',1658376000,'Audience Display and Points',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8118f5c5-9570-4e93-ab07-9262ca30b3bb','a722008f-f269-4018-b755-b25cd2c5471a',1658376000,'Website (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('55120798-2208-48e9-b617-804e595f35e7','a722008f-f269-4018-b755-b25cd2c5471a',1658462400,'Race Day',7.0,20.0,140.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('327193a6-9393-498e-8bb3-caff95069727','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690171200,'Website work and graphics',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8ff3daae-0a76-4df9-b94f-7e1aa954a3aa','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690257600,'Website backend (off-site)',3.5,20.0,70.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('d2684ffe-51ab-4f20-8539-b7d1a1b76f87','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690344000,'Headshots and placeholders',5.5,20.0,110.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('c9bf1a18-b45e-4c5f-abf4-34cf709fe689','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690430400,'Lineups and auth security (off-site)',4.5,20.0,90.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('97cc7b06-caa1-4a75-b1f7-3f95ab0b5e19','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690516800,'Audience display, news editor, prices',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('6d9aeec4-a3de-4faa-be35-feacdb39e350','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690603200,'Price editor, begin database migration',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('eab32b2d-9edb-4a71-ad92-11d872857be9','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690689600,'Database migration, match up 2022 reg',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('0c133b57-d722-47a0-b390-c7ada5e555d9','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690776000,'Begin express registration (auto) (on-site)',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('3e8072e3-d462-492c-a32e-5bafa12ac66d','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690776000,'Finalize express registration (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('1ef0a686-5c33-4bce-892e-b72cb4f6528a','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690862400,'Champion bios, rework points for new DB',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('9f75cdeb-4833-4325-abdf-f392c8be311b','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1690948800,'Race Day',8.5,20.0,170.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8ca5bc48-ae7c-458d-9ae5-da54edb580bd','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691035200,'Website hotfixes',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('bf316f01-6d97-4887-bbb4-7f6bc04e1075','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691121600,'Tire/Office swap, website final touches',5.5,20.0,110.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('37f86e75-a336-4f7b-ae0c-345dd584d1a1','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691208000,'Race Day/Website publish',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ddedea49-53f5-4d91-913d-48156ac2b4cc','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691380800,'Database fixes (on-site)',3.5,20.0,70.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('a1315055-0a28-4a70-9701-433201cd4870','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691380800,'Draft wall of champions page',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('4e858515-b73a-411e-90ca-605b396c7d9c','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691553600,'Webcam/Wall of champions (on-site)',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('e8121138-1871-41f0-8904-0f43ce5e4690','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691553600,'Wall of Champions Finalize/Publish',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('480dca64-e20f-43a9-8b6c-77acd8902f3d','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691640000,'Migrate to managed DB/Hall of Fame',7.5,20.0,150.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('34d04265-a18f-4ec7-9031-7141fe411c28','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691726400,'WiFi install, laptop setup, Add results to site',4.5,20.0,90.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ce8e3f34-5c0b-4e51-80f9-13ef76a05e74','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691812800,'Race Day',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('771c15eb-0062-43ca-9a72-cc76069cd02a','e18f8253-59a5-45ab-9070-8397930c8e12',1692676800,'Points repair',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('dfe1d780-2ce9-4aa3-bafe-692dfc5e4e3f','e18f8253-59a5-45ab-9070-8397930c8e12',1692936000,'Add JuiceBox division to site',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('d925981d-a0cc-4902-83e4-72cea6400014','e18f8253-59a5-45ab-9070-8397930c8e12',1693022400,'Prep site for ISP300 ticket/reg sale',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('7ac101d2-21a3-423b-ae3e-d7e796cad4cb','e18f8253-59a5-45ab-9070-8397930c8e12',1693972800,'Bring up old database site',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('30633312-1e3a-4bd6-9e36-d56a0c455a7c','e18f8253-59a5-45ab-9070-8397930c8e12',1694750400,'Fix registration car check',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('88f0b570-6406-42c8-adcb-fedd96bcbd1f','e18f8253-59a5-45ab-9070-8397930c8e12',1694836800,'Implement season ID system',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('cbc4efa9-f68e-44f9-b535-eee608c54a9e','e18f8253-59a5-45ab-9070-8397930c8e12',1695268800,'Update website content manager',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e0e20b6f-dcd0-442c-a6c1-108c8a2d4c44','e18f8253-59a5-45ab-9070-8397930c8e12',1695355200,'Add toggle to event visibility, update events',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e8a37197-cf61-4e50-a6ef-f4bc16caf583','e18f8253-59a5-45ab-9070-8397930c8e12',1696305600,'Design/implement BCA month graphics',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('53e884d4-0e44-47ae-9de1-a08e502166d8','e18f8253-59a5-45ab-9070-8397930c8e12',1696392000,'Create special event season/reg',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('d275522f-1659-473a-badc-70abe80aeb07','e18f8253-59a5-45ab-9070-8397930c8e12',1696478400,'Special event roster viewer',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('ccc37a05-5b94-4e8b-a804-167d7d86664e','e18f8253-59a5-45ab-9070-8397930c8e12',1696564800,'Add fee/payment process to special events',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('920b3a78-9957-4c32-b5ed-df46c297e5fc','e18f8253-59a5-45ab-9070-8397930c8e12',1696910400,'Email update (hide personal data from all)',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('85ff6150-7947-41d8-b408-1a816aa0fc76','e18f8253-59a5-45ab-9070-8397930c8e12',1697601600,'Update internal roster viewer for full data',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('8d4d28e2-db87-4a36-891c-2cee4b161bc9','e18f8253-59a5-45ab-9070-8397930c8e12',1697688000,'DB sanitization, prep for export 1099',1.5,20.0,30.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('0d68af50-8887-467a-b1d0-1071e2c479e3','e18f8253-59a5-45ab-9070-8397930c8e12',1698206400,'Add special event roster viewer to site',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('c84b3e48-f1b6-4199-bcdd-6f8685b2774f','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1698811200,'SE roster, change theme, update events.',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e9a56413-22c6-4736-8e60-d510bb2ae953','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1698897600,'SE roster visibility, live DB detection',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('aa720f49-b51a-437b-a413-4a9f6a4f9544','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699070400,'CMS RosterView Update',7.0,20.0,140.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('bf2c1ba6-8d19-4280-84ce-8173b863c23c','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699246800,'CMS Backend Redesign (OOP)',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('704f9d4a-27d6-4b25-af11-43ac8211959b','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699333200,'Various DB/Roster updates/exports',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('2bc45810-c0bb-4150-9191-e27efa42d7c4','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700197200,'Shopify Website Design/Setup',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('208cea71-e378-494d-bcff-92c19ead51b7','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700370000,'Special Event Mail Merge',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('a5f209a2-a65e-4e29-a137-4381bb477327','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700456400,'Special Event Envelope Automation',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('95d8a07d-a5c2-4453-9795-c35cc7fc82b3','352863b6-4bcd-4060-9aee-7a1493381646',1701752400,'Compress all images for quicker site load',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('02d59723-bdd7-4bba-ba74-adfa0cfc7a16','352863b6-4bcd-4060-9aee-7a1493381646',1701838800,'Begin banquet registration',3.5,20.0,70.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('c58588d4-ac5b-424b-98ad-340157190c5e','352863b6-4bcd-4060-9aee-7a1493381646',1702357200,'Banquet registration database setup',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('407e13d3-4e35-4ec4-a9a0-95c0916193a0','352863b6-4bcd-4060-9aee-7a1493381646',1702443600,'Banquet reg stripe price generation',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('093d4237-83de-4d7f-9e5c-42a719726a03','352863b6-4bcd-4060-9aee-7a1493381646',1702616400,'Online store theming/UI',3.5,20.0,70.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('eaad59e0-1324-4a00-b443-d614fd56a227','352863b6-4bcd-4060-9aee-7a1493381646',1702702800,'Online store pricing/payment',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('36228c80-bc0a-4940-b088-4904b17899e7','352863b6-4bcd-4060-9aee-7a1493381646',1703566800,'Finalize banquet registration',5.5,20.0,110.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('a694d9db-b50c-4863-b552-c80b19f53222','352863b6-4bcd-4060-9aee-7a1493381646',1703653200,'Update champions and win tallys',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('22612fae-9421-40d9-900e-643638ca7531','352863b6-4bcd-4060-9aee-7a1493381646',1703826000,'Show prev rosters, add announcements',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('b39974ea-cd78-4271-b6f0-60c9b8c4911c','352863b6-4bcd-4060-9aee-7a1493381646',1703912400,'CMS banquet roster visibility',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('06bc2406-bbbe-4daa-96d7-d80151aa41e0','352863b6-4bcd-4060-9aee-7a1493381646',1704171600,'Hide registration for fixes, refund users',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('1adee95a-f05a-4f2c-b648-ee1af13ed1ff','352863b6-4bcd-4060-9aee-7a1493381646',1704517200,'Convert to store-pay-update for 2024 reg',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('066c172c-debd-4227-bbc1-0e4eb8d4d74e','352863b6-4bcd-4060-9aee-7a1493381646',1704603600,'Finalize and publish 2024 registration',5.0,20.0,100.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('3ae082a5-3e30-401c-8757-29306ae32dae','352863b6-4bcd-4060-9aee-7a1493381646',1704776400,'New events editor, disable letters for 2024',7.0,20.0,140.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('2b5c2d59-4611-4f69-8859-3f7e7d3b294e','352863b6-4bcd-4060-9aee-7a1493381646',1704862800,'Rules uploader',8.0,20.0,160.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('05304002-9b6c-423b-bbee-4637d67041a5','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1704949200,'In-person Track Day',7.5,20.0,150.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('fd6e3b70-9198-4aa2-be41-f2186bfeb52a','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705035600,'Banquet export and mail merge',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('4f662168-ed58-4fa5-99ae-d79eeeae201e','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705208400,'Number reservations',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('90373154-537e-43a0-82a8-fcc036514461','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705381200,'Division page hotfix',0.5,20.0,10.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('67fc6ed2-e430-4e28-90dc-c40bd7c2e3b4','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705467600,'Auto display driver registrations',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6d680a14-f864-4047-8c3b-ff6afcdaf10c','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705640400,'Shopify Finances',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('c7ca7f6d-bba8-436f-9ecc-13b7e67993c7','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705813200,'Banquet Mail Merge pt.2',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('18154b9a-0377-48ee-b3b6-64e0aafa45ff','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705813200,'Banquet ticket close/clean up',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6f2849cc-65d9-44fe-8b12-82c551fa71a2','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1706245200,'Take down schedule, fix event publisher',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f0a627d2-5f3a-4a9c-ab54-f7da5a304b00','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1706590800,'Permissions, sponsor links',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f0349c88-43ff-4dba-9cfe-5940713b1612','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707022800,'Begin new roster viewer/editor',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('fcafdfbb-6f33-44d0-8044-4450b772b061','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707109200,'Roster editor UI/Tables',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f3b99670-1f60-4e87-bb80-95170ddd784f','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707368400,'Roster editor,change participants/autofill',4.5,20.0,90.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('10134ca6-0e8a-4c41-a91b-13945a12a4cb','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707454800,'Roster editor,Auto tax form generation',6.5,20.0,130.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('a3b9aaec-8ba1-49fc-b1a9-7506fd84460a','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707627600,'Update CMS navigation',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('52460b23-e519-4fc6-ac89-46576070f9f3','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707714000,'CMS User Manager/Perms editor',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('c80150ba-34fb-4b9b-a9a5-78024e7b5e40','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1708837200,'NASCAR Reg Link, general typos',0.5,20.0,10.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('4c88ca6d-482e-489c-9da9-16fa2cc8bd00','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1710129600,'Track day',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('5c8995f6-b191-4ba6-b129-0537785e156e','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1710216000,'Event page custom links',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('0b2c3bc2-be20-4c16-b384-9d5bd1e2e693','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710388800,'Track Day',3.5,20.0,70.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('78553d71-aa77-4791-8ec1-0d2b43973308','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710475200,'Remote Onedrive Support',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('e7902386-266d-4b8a-85ce-47851e181d02','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710907200,'Data collection/analysis for site',2.5,20.0,50.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('23c59227-54d9-43ad-9b34-a554b52ba74f','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711339200,'Driver 1099/W-9 generation update',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('d18e53f0-ead0-4b56-b56c-be2b7671e7ea','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711425600,'Itinerary search/export',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('51f1ebdd-f68b-40c6-83b7-d3b413882360','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711512000,'Itinerary resend, Reg data export/merge',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f4145173-a276-458c-a8d3-c8b94b5c4cf5','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711598400,'Fix itinerary missing from website',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6dd9e3c8-7def-48a2-840b-a72de7e1c753','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711944000,'Roster/Itinerary updates',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('61b692a2-8c63-4061-9f35-30844a2cedd1','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712548800,'Roster download link',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('a571bef7-b402-4316-b4db-209679d67fed','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712721600,'Roster phone number export patch',2.5,20.0,50.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('38290567-dc1f-420c-8a74-1fda829e218d','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712808000,'Stripe support contact/ticket',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('5835c23b-3872-45e8-b7fc-1e9884313a26','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1713153600,'Credit card charge match with stripe',1.5,20.0,30.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f7e8504f-a95c-4921-9859-6f5c0687b1ad','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1714017600,'Exit cleanup/account reassignment',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('02602261-24d0-4546-88da-ff9fb14c3eed','1942364d-df4e-4175-8210-dbc202ca1038',1733979600,'Begin racehub-next development',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('c6334fbb-6892-4760-a61c-5cdc04921c72','1942364d-df4e-4175-8210-dbc202ca1038',1734066000,'Migrate basic features, authentication',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('33957386-0976-4800-a01d-2a5977e8df2a','1942364d-df4e-4175-8210-dbc202ca1038',1734498000,'Logistics planning and roadmap',1.0,25.0,25.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('dacda1bb-a445-4cdc-bdc5-db3bd1f48de1','1942364d-df4e-4175-8210-dbc202ca1038',1734670800,'Change racehub-php season, begin DB',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d5bddee3-1892-4c7b-bab9-50598fcf7d83','1942364d-df4e-4175-8210-dbc202ca1038',1734757200,'Events page integration, rich homepage',5.5,25.0,137.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('3d80aba0-53d9-40d9-a163-5dc6aff36320','1942364d-df4e-4175-8210-dbc202ca1038',1734930000,'Create news page, optimize loading flow',5.0,25.0,125.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('f21fba61-87c1-4426-af99-450e42c193f5','1942364d-df4e-4175-8210-dbc202ca1038',1735016400,'Begin DigitalOcean provisioning/deploy',2.5,25.0,62.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('86ec004d-cc98-4c37-9943-1a7f60170d69','1942364d-df4e-4175-8210-dbc202ca1038',1735189200,'Deploy app/DB, news page optimizations',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('ab7cc962-5024-48e6-979a-885ccf6a7194','1942364d-df4e-4175-8210-dbc202ca1038',1735275600,'Fix deployment issues, integrate DO App',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('08d9e276-6dcf-4e50-9de5-dd13b580fe6f','1942364d-df4e-4175-8210-dbc202ca1038',1735362000,'Add image compression, content delivery',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('6a243898-3ef9-47f0-9008-9e3fca0a1c33','1942364d-df4e-4175-8210-dbc202ca1038',1735448400,'Announcements, Promo, Sponsors CMS',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('23059eac-34fb-44d2-9c36-1e10e387167d','1942364d-df4e-4175-8210-dbc202ca1038',1704171600,'Begin competitors page',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('9758e516-7655-4410-9f12-b069326ff3e2','1942364d-df4e-4175-8210-dbc202ca1038',1704258000,'Migrate APIs to tRPC for data security',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('628da274-f479-4603-bde2-9556795a6d4d','1942364d-df4e-4175-8210-dbc202ca1038',1704344400,'Recreate articles CMS for rich text',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('12a7c5ef-4029-410f-b176-a52966015698','1942364d-df4e-4175-8210-dbc202ca1038',1704430800,'Migrate announcements editor, add raindate',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('bea4d148-602b-4cc6-a1a9-4b9a7717c050','1942364d-df4e-4175-8210-dbc202ca1038',1704517200,'Discuss and plan out site scope (In-person)',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d286c494-64c4-4eaf-9a8d-5ad681b4413b','1942364d-df4e-4175-8210-dbc202ca1038',1704517200,'Implement reports, rules, and champs',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d43cedf4-a854-44ef-8853-725369212bd6','1942364d-df4e-4175-8210-dbc202ca1038',1704603600,'Add CMS authentication, route protection',6.5,25.0,162.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('fb557beb-9912-4e63-b883-8ff74451062b','1942364d-df4e-4175-8210-dbc202ca1038',1704690000,'Clean up deployment, fix UI/display bugs',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('1383248a-2301-4df4-985d-042cd44c1c49','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736398800,'Correct rain date and sponsor editor saves',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('dff81591-7781-45a2-b7b4-2e729c15048b','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736485200,'Fix bugs with article editor and images',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('01f8a30d-e04e-4ccc-ad18-da918e677ff9','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736571600,'Add upload event image/compress for load',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d2f51448-c17c-4dc1-bfb3-09f7af3f9d3a','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736744400,'Work w/ hotlap to get registration roster',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('70f9a81a-a4c6-4c78-b80e-0b5a6b0123a0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736830800,'Add user management w/ email pwd reset',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b06d988c-abb5-40a7-baad-f35878cf11e9','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736917200,'Finalize code for public, deploy site, bkp old',6.5,25.0,162.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b6ef7b4b-f43a-472d-abbb-49031e268e88','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737003600,'Add analytics for page views and clicks',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('4081c2cd-2af2-4283-9e37-5992557666c7','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737090000,'Track System Setup/Shopify (In-person)',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('29a894b6-46c4-4a01-a7c8-4ebe0fc9c0cd','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737176400,'Begin real-time banquet voting system',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b8a10fea-3e9b-4885-ae1c-ef222a6584e4','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737435600,'extract/export W9 information for 2024',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d78e4072-375c-41e2-8a81-69b7380b9d30','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737522000,'Implement 2024 roster for voting',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('735d00db-dd71-48a2-81dc-d4ab34dc3733','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737608400,'Test and complete deployment of voting',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('aa9f9359-04a1-4b47-8515-dec844564502','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737694800,'Push and enable banquet voting, fix bugs',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('881a19bf-b655-407d-9a52-1639ce13c5fe','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737781200,'Remove banquet voting, show points tables',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('c07aed16-5c22-4fde-9476-b8a8a7485572','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737867600,'CMS Reconfiguration for SS and MS class',2.5,25.0,62.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('726427ac-a5f0-4c05-9efd-0402fa6e30f0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737954000,'Competitors and division page redesign',5.0,25.0,125.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('874a1159-df26-4851-8dc5-d34509b25e77','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738040400,'Browser conflict tests and fixes',3.5,25.0,87.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b0fb99d3-9c32-4729-89ce-7aab0ba98256','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738299600,'Rules CMS Editor upload and edit repair',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('33cda6b9-cdc6-4211-a52f-a6aa9badaf2f','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738558800,'Migrate backup from BB to DO, sys updates',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d1dfaa3d-c880-47c4-b2a8-5e1c61b72ae0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738645200,'Create and verify backup scripts',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('21627b66-05b7-472e-8df6-ddc37554bf3b','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738731600,'Optimize devenv to use locally hosted S3/DB',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('271561b0-b8af-4603-aa43-49ba87bc4da6','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738904400,'Verify integrity of backup change chunks',1.0,25.0,25.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('f4d05559-46e7-46da-8cf0-00606e63fb49','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1741150800,'Limit event display, update event layout',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('b7a66b38-1628-46cd-be21-0d9d0f7c105a','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1741579200,'Work w/ cloudflare to inc. file size limit',1.0,25.0,25.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8a98405f-ff6b-4e64-83aa-25cf2ad0e3cb','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742097600,'update/fix article saving/loading process',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('80c17c33-bc48-44e3-b358-73dc7df0b63e','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742184000,'update/fix rule saving/loading process',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('d8f3066a-ea93-4221-8d9e-1921fb31d006','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742270400,'patch Next.JS emerg. security vulnerability',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('41c658b0-8020-4471-9e8d-e0f67108c9a9','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742356800,'Update server headers to use new limit',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('933bad4e-f7da-452c-b8f5-be6d631cbe23','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742702400,'Add PDF export of events/rules on demand',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('d204cb6a-be0e-4ee7-8c46-a7f532c7a291','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742788800,'Add file caching to save $ on server usage',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('7e172229-4a68-482c-b429-326e228d185e','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742875200,'Add video upload, begin driver testimonial',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('19c8f1e4-e676-40c2-ba1a-c370c2491af8','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742961600,'Disable points section, prep for new points',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('1c26eeb4-22de-47bd-a170-d003fda1a213','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1743048000,'Finalize testimonial, update/enable points',5.0,25.0,125.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c5aaf396-c27b-44ac-b141-c69872d87a4d','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1743393600,'Retrieve and display previous itineraries',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('df365fb2-9d75-4589-83e4-48969e62df5d','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746244800,'Lineups upload interface finalized/pushed',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('0a35f2d9-15b1-4d82-9ba1-df27f0024f6f','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746331200,'Lineup audience display',3.75,25.0,93.75,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6d6502d2-0f0d-4521-8943-4ae78e5bc7d9','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746417600,'Lineup mobile display',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('258f123f-f80b-4920-af38-08bc8d163f5e','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746849600,'Begin points upload system backend',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c517d481-9741-4283-b74c-e61b500cfd2c','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747108800,'FileMaker points parsing logic',2.0,25.0,50.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('4e5e6815-6b4b-4433-8a52-dafcbcdd7284','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747195200,'Update spectator policy system',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('fa70e962-0678-4593-b8fd-8abab5a26c6b','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747454400,'Restructure lineup page logic for old phone',3.5,25.0,87.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('9e1878f9-f485-408e-91c9-281b02737d3e','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747540800,'Handle cross time zone errors w/ lineups',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8effbfe4-1434-4448-b7a6-5ab316fc93f9','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747800000,'Crate mod points issue fix',1.0,25.0,25.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('12ea8e17-eac6-42b4-aa22-3981003172a5','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748404800,'In person, website/network planning',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c8bd20a4-19d0-47ca-b381-93bd6e5fd2dc','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748491200,'Rain date API integration/management',5.0,25.0,125.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('02ce3809-d900-4f1f-9400-64b225d61339','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748577600,'Begin lineup patches for visibility',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8f4d886d-8d3d-4a30-bec7-b41ee854f731','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748664000,'Bind rain dates to events, show reschedule',5.5,25.0,137.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6fce0331-9208-408f-8369-4fb4a2fb2fa4','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748750400,'drag and drop lineups, divisions cms update',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6f49db90-fa25-44ab-9ef4-57c00c9c36c3','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748836800,'home page reordering, QoL improvements',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c63d9d91-6e0c-48f8-b2f1-a02c4839848c','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748923200,'In person, bulk email system',3.5,25.0,87.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('61b3faf1-4edc-4b05-9914-45fa8b49b51f','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748923200,'Remote, bulk email/delta points',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c236b466-5706-4bad-8324-5219c17dd2f2','06c43197-9685-4116-b83b-1c76840905ab',1652500800,'Replay Operator',10.0,40.0,400.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('772bdeaa-a5a9-4c7e-8a54-d02b6d115e16','a66739ec-fbfe-4871-8388-0b34b2228889',1683777600,'Install and configure tech PCs',2.0,20.0,40.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('d88ffb8e-4c29-4882-8dff-dd2d227b1639','a66739ec-fbfe-4871-8388-0b34b2228889',1683950400,'Tire shack sales/maintenance',2.0,20.0,40.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('66eeb92c-ecf3-46c1-b6f6-6569b90fe598','a66739ec-fbfe-4871-8388-0b34b2228889',1684123200,'Tire program/scanning',1.0,20.0,20.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('8f00b60d-5dc7-4f19-ad3e-2a51d1c4d296','d6a1da99-d066-4993-b907-1e30a769f107',1743652800,'Correct time-zone errs for non-EST viewers',2.0,25.0,50.0,1752274548,0);
INSERT INTO beenvoice_invoice_item VALUES('3ab632b7-cebc-49a0-8f59-9f39db3c9543','d6a1da99-d066-4993-b907-1e30a769f107',1743912000,'WiFi Setup/Security Updates across sites',2.0,25.0,50.0,1752274548,1);
INSERT INTO beenvoice_invoice_item VALUES('a3cc32fd-a0aa-4986-8ec6-91e6572ed13d','d6a1da99-d066-4993-b907-1e30a769f107',1744084800,'Standardize date handling, data utility upd.',3.5,25.0,87.5,1752274548,2);
INSERT INTO beenvoice_invoice_item VALUES('f5009d53-27e0-4104-bde3-afaeb4c924e7','d6a1da99-d066-4993-b907-1e30a769f107',1744776000,'Rephrase/reorganize home page',2.5,25.0,62.5,1752274548,3);
INSERT INTO beenvoice_invoice_item VALUES('d92fb22e-e0fb-4e82-b2f9-27f8eee5a150','d6a1da99-d066-4993-b907-1e30a769f107',1744862400,'Add ability to remove/submit null timeslots',3.0,25.0,75.0,1752274548,4);
INSERT INTO beenvoice_invoice_item VALUES('a2228d31-0c4c-49f7-ba7c-a09eb4dfe2c5','d6a1da99-d066-4993-b907-1e30a769f107',1744948800,'Hostway email contact investigate/upload',2.5,25.0,62.5,1752274548,5);
INSERT INTO beenvoice_invoice_item VALUES('c5dcc389-3fea-4cfa-98eb-2130016be99a','d6a1da99-d066-4993-b907-1e30a769f107',1745035200,'Re-render live schedule, update deps.',4.0,25.0,100.0,1752274548,6);
INSERT INTO beenvoice_invoice_item VALUES('73386e72-750e-4eb1-83de-e239c66102fe','d6a1da99-d066-4993-b907-1e30a769f107',1745467200,'Add rich text editor to site backend',3.5,25.0,87.5,1752274548,7);
INSERT INTO beenvoice_invoice_item VALUES('87aa98a8-131d-49bb-98fb-0460a8dde4ab','d6a1da99-d066-4993-b907-1e30a769f107',1745553600,'Update mobile view, fix rules pagination',2.0,25.0,50.0,1752274548,8);
INSERT INTO beenvoice_invoice_item VALUES('6fe10405-029e-4164-b918-f521d3830818','d6a1da99-d066-4993-b907-1e30a769f107',1745812800,'Lineups backend port from racehub-php',2.0,25.0,50.0,1752274548,9);
INSERT INTO beenvoice_invoice_item VALUES('62f2594f-0d24-405a-989c-2fcb5392a3e6','d6a1da99-d066-4993-b907-1e30a769f107',1745899200,'Update filemaker, add csv export/import',2.5,25.0,62.5,1752274548,10);
INSERT INTO beenvoice_invoice_item VALUES('208eebce-58e5-4d1a-8088-47a516fe39c9','d6a1da99-d066-4993-b907-1e30a769f107',1745985600,'Wireframe/basic lineups user interface',3.5,25.0,87.5,1752274548,11);
INSERT INTO beenvoice_invoice_item VALUES('cf1c9e48-bf50-4083-b482-9338a3c439d0','0c9a6715-70f8-4f83-ab01-a8340773431d',1749096000,'Enhance PointsUpload page',3.5,25.0,87.5,1752278188,0);
INSERT INTO beenvoice_invoice_item VALUES('212d7b08-2d12-449a-a0f9-c4496819b740','0c9a6715-70f8-4f83-ab01-a8340773431d',1749441600,'Handle ties in points section',3.5,25.0,87.5,1752278188,1);
INSERT INTO beenvoice_invoice_item VALUES('0d6d372f-6679-4dea-b78b-03ef0192c1e4','0c9a6715-70f8-4f83-ab01-a8340773431d',1749528000,'Add manipulation of bulk email contact lists',4.0,25.0,100.0,1752278188,2);
INSERT INTO beenvoice_invoice_item VALUES('58dfc4ef-8498-4630-a62f-b5fd20410e6e','0c9a6715-70f8-4f83-ab01-a8340773431d',1749614400,'Add staff list to email system, create new',3.5,25.0,87.5,1752278188,3);
INSERT INTO beenvoice_invoice_item VALUES('513c952b-c0f7-49ee-948d-41e5ca4d6e83','0c9a6715-70f8-4f83-ab01-a8340773431d',1749700800,'Add rain banner functionality to events',4.0,25.0,100.0,1752278188,4);
INSERT INTO beenvoice_invoice_item VALUES('469256a8-8335-48ce-a001-67928accf01c','0c9a6715-70f8-4f83-ab01-a8340773431d',1750046400,'Social Media code of conduct',2.0,25.0,50.0,1752278188,5);
INSERT INTO beenvoice_invoice_item VALUES('30720638-2128-4017-897a-8d635d541246','0c9a6715-70f8-4f83-ab01-a8340773431d',1750219200,'Active status management, event cleanup',3.75,25.0,93.75,1752278188,6);
INSERT INTO beenvoice_invoice_item VALUES('d403fc8d-72d3-4d75-a91e-9b3cf68df820','0c9a6715-70f8-4f83-ab01-a8340773431d',1750305600,'Google/Apple Calendar Sync from events',4.5,25.0,112.5,1752278188,7);
INSERT INTO beenvoice_invoice_item VALUES('217f013d-861a-406e-bd8e-392659f6ba72','0c9a6715-70f8-4f83-ab01-a8340773431d',1750392000,'In person, printers/email/server updates',5.0,25.0,125.0,1752278188,8);
INSERT INTO beenvoice_invoice_item VALUES('52be1c1f-3523-4bc3-a8ab-66902db5e229','0c9a6715-70f8-4f83-ab01-a8340773431d',1750478400,'Race day, Server/Handicapping',6.0,25.0,150.0,1752278188,9);
INSERT INTO beenvoice_invoice_item VALUES('dee51491-b6b1-4038-a641-d4fcdfe42f95','0c9a6715-70f8-4f83-ab01-a8340773431d',1750651200,'Repair sponsors/Plan out permissions',3.5,25.0,87.5,1752278188,10);
INSERT INTO beenvoice_invoice_item VALUES('0bd1bec4-2541-42db-ae38-d86d9bac43d5','0c9a6715-70f8-4f83-ab01-a8340773431d',1750737600,'Backend permissions implementation',5.5,25.0,137.5,1752278188,11);
INSERT INTO beenvoice_invoice_item VALUES('dbcb12d5-9b37-4f65-9275-56d82338601b','0c9a6715-70f8-4f83-ab01-a8340773431d',1750824000,'Frontend permissions/deployment',5.0,25.0,125.0,1752278188,12);
INSERT INTO beenvoice_invoice_item VALUES('2899f8ae-6f76-4f32-8350-09151b3d76ab','0c9a6715-70f8-4f83-ab01-a8340773431d',1750910400,'Plan out and begin migration to races sys',4.5,25.0,112.5,1752278188,13);
INSERT INTO beenvoice_invoice_item VALUES('f6f46a67-83ac-4bb1-b128-82daf0063128','0c9a6715-70f8-4f83-ab01-a8340773431d',1750996800,'Replace eventDivisions with races',5.0,25.0,125.0,1752278188,14);
INSERT INTO beenvoice_invoice_item VALUES('56e676ae-3de1-4039-b3d6-e5da99c5aa0c','0c9a6715-70f8-4f83-ab01-a8340773431d',1751083200,'In person, race day, media, development',8.0,25.0,200.0,1752278188,15);
INSERT INTO beenvoice_invoice_item VALUES('71fb8bc8-ac75-426b-a624-83bbaebbac1c','0c9a6715-70f8-4f83-ab01-a8340773431d',1751169600,'User interface for race editing',5.5,25.0,137.5,1752278188,16);
INSERT INTO beenvoice_invoice_item VALUES('eb64faf3-2a9b-4f66-8dd9-4f39f6a7af05','0c9a6715-70f8-4f83-ab01-a8340773431d',1751256000,'Public user interface for finishes and lineup',5.5,25.0,137.5,1752278188,17);
INSERT INTO beenvoice_invoice_item VALUES('79b80323-6c8a-4562-a274-f9e697b1efe4','0c9a6715-70f8-4f83-ab01-a8340773431d',1751342400,'Production push pt.1',6.0,25.0,150.0,1752278188,18);
INSERT INTO beenvoice_invoice_item VALUES('cd84469d-f608-4edd-9121-4366041fe25a','0c9a6715-70f8-4f83-ab01-a8340773431d',1751428800,'Production database migration',3.0,25.0,75.0,1752278188,19);
INSERT INTO beenvoice_invoice_item VALUES('93d21511-d3f9-4338-8eb4-3233614c4ae0','0c9a6715-70f8-4f83-ab01-a8340773431d',1751774400,'Testing, data entry from old races begin',4.0,25.0,100.0,1752278188,20);
INSERT INTO beenvoice_invoice_item VALUES('e701eb75-8ce0-4194-812a-2a3520487a00','0c9a6715-70f8-4f83-ab01-a8340773431d',1751860800,'Update pricing queries, 2023 races',4.0,25.0,100.0,1752278188,21);
INSERT INTO beenvoice_invoice VALUES('76d570fe-bfec-47bd-a7fa-b4ee8133c78e','INV-20210417-131231','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1618617600,1621209600,'paid',220.0,'Imported from CSV: 2021-04-17.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132158,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('61c3d28c-5031-4372-86e3-5bf895411046','INV-20210508-131255','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1620432000,1623024000,'paid',320.0,'Imported from CSV: 2021-05-08.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132159,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('57fcd73a-0876-4e91-9856-0f9c9695fcd1','INV-20210605-131278','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1622851200,1625443200,'paid',300.0,'Imported from CSV: 2021-06-05.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132159,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2','INV-20210714-131301','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1626220800,1628812800,'paid',510.0,'Imported from CSV: 2021-07-14.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132159,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('4fb5d8be-2588-4187-955d-e7643b08619f','INV-20210807-131324','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1628294400,1630886400,'paid',280.0,'Imported from CSV: 2021-08-07.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132160,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('f48104da-1baa-4a70-9d0c-c03f4017f60d','INV-20210825-131337','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1629849600,1632441600,'paid',450.0,'Imported from CSV: 2021-08-25.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132160,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5','INV-20210921-131348','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1632182400,1634774400,'paid',340.0,'Imported from CSV: 2021-09-21.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132160,1752133428,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('6c4314c7-7bc7-4d8a-9513-59a1ebcfd890','INV-20211201-131360','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1638316800,1640908800,'paid',200.0,'Imported from CSV: 2021-12-01.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132161,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('b018eaca-b4b1-4c96-8e40-2a1ab5211e48','INV-20220422-131373','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1650585600,1653177600,'paid',250.0,'Imported from CSV: 2022-04-22.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132161,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('a0da2a05-5681-46fd-b988-235ec24971e2','INV-20220514-131387','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1652486400,1655078400,'paid',200.0,'Imported from CSV: 2022-05-14.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132162,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('713a368a-f7de-4de8-95dd-2a4a2d626fa1','INV-20220521-131401','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1653091200,1655683200,'paid',540.0,'Imported from CSV: 2022-05-21.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132162,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('fac3b7e2-9816-459c-960e-ac520b3f2cd5','INV-20220607-131419','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1654560000,1657152000,'paid',460.0,'Imported from CSV: 2022-06-07.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132162,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('8704d2fe-8972-4dae-8062-2f5b81e14493','INV-20220630-131436','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1656547200,1659139200,'paid',600.0,'Imported from CSV: 2022-06-30.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132163,1752133428,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('babfc847-b37d-44f2-91a9-4251691c11b4','INV-20220731-131453','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1659225600,1661817600,'paid',820.0,'Imported from CSV: 2022-07-31.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132163,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('89f677fb-ca0f-4d43-9547-d4da77f0f0ba','INV-20230316-131472','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1678924800,1681516800,'paid',520.0,'Imported from CSV: 2023-03-16.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132163,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('2a07bf2e-1923-4b4b-aba9-14c507a2f2c4','INV-20230513-131490','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1683936000,1686528000,'paid',750.0,'Imported from CSV: 2023-05-13.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132164,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('0b057a65-fe7d-4495-8756-4dd61f6895e1','INV-20230521-131513','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1684627200,1687219200,'paid',790.0,'Imported from CSV: 2023-05-21.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132164,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('f86f4002-6539-44a3-b8c9-ca6689f809c1','INV-20230604-131532','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1685836800,1688428800,'paid',1050.0,'Imported from CSV: 2023-06-04.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132164,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('ef6a5079-2d65-46b1-8d87-a9ef5c0cb650','INV-20230611-131552','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1686441600,1689033600,'paid',540.0,'Imported from CSV: 2023-06-11.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132165,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb','INV-20230709-131574','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1688860800,1691452800,'paid',800.0,'Imported from CSV: 2023-07-09.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132165,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('9186435f-2b62-4c58-aa45-c00aeac9c7d6','INV-20230717-131599','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1689552000,1692144000,'paid',910.0,'Imported from CSV: 2023-07-17.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132165,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('a722008f-f269-4018-b755-b25cd2c5471a','INV-20230722-131624','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1689984000,1692576000,'paid',720.0,'Imported from CSV: 2023-07-22.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132166,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('ed3cf514-1438-4ee0-8e72-3f47c0f9aa15','INV-20230801-131649','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1690848000,1693440000,'paid',990.0,'Imported from CSV: 2023-08-01.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132166,1752133428,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('c7e84ee9-ae1e-4f31-b120-6cc7e02b0442','INV-20230812-131677','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1691798400,1694390400,'paid',1130.0,'Imported from CSV: 2023-08-12.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132166,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('e18f8253-59a5-45ab-9070-8397930c8e12','INV-20231025-131707','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1698192000,1700787600,'paid',730.0,'Imported from CSV: 2023-10-25.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132167,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('f39a6380-e1c0-4a28-b25e-f960e40ebbdc','INV-20231120-131737','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1700438400,1703030400,'paid',570.0,'Imported from CSV: 2023-11-20.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132167,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('352863b6-4bcd-4060-9aee-7a1493381646','INV-20240110-131769','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1704844800,1707436800,'paid',1150.0,'Imported from CSV: 2024-01-10.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132167,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('dc0e0595-07a8-471b-8f7b-23cd13c0b8c1','INV-20240314-131797','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1710374400,1712966400,'paid',1190.0,'Imported from CSV: 2024-03-14.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132168,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('cf6ea6c8-c485-4a01-aa12-f68306ef426a','INV-20240425-131828','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1714003200,1716595200,'paid',660.0,'Imported from CSV: 2024-04-25.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132168,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('1942364d-df4e-4175-8210-dbc202ca1038','INV-20250108-131858','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1736294400,1738886400,'paid',2100.0,'Imported from CSV: 2025-01-08.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132169,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('547569b8-2f7c-486b-a4f1-2a7b80aa904a','INV-20250207-131897','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1738886400,1741478400,'paid',1925.0,'Imported from CSV: 2025-02-07.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132169,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('bd64542e-c576-4dd7-b0d4-f4d6077aef25','INV-20250402-131932','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1743552000,1746144000,'paid',850.0,'Imported from CSV: 2025-04-02.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132170,1752133428,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('d6a1da99-d066-4993-b907-1e30a769f107','INV-20250501-132029','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1746057600,1748649600,'paid',825.0,'Imported from CSV: 2025-05-01.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132170,1752274548,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('5a8f214c-8f6d-46e9-949e-1e9e31c40974','INV-20250604-132064','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1748995200,1751587200,'paid',1506.25,'Imported from CSV: 2025-06-04.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132170,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('0c9a6715-70f8-4f83-ab01-a8340773431d','INV-20250702-132103','1c17bccd-3bc6-42c2-a500-68728a2a9d25',1751414400,1754006400,'sent',2481.25,'Imported from CSV: 2025-07-02.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132171,1752278188,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('06c43197-9685-4116-b83b-1c76840905ab','INV-1752132853225','8c24c053-9f84-49be-95e3-30fe9cdcdeef',1652500800,1655179200,'paid',400.0,'Imported from CSV: 2022-05-14-NBC.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132902,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
INSERT INTO beenvoice_invoice VALUES('a66739ec-fbfe-4871-8388-0b34b2228889','INV-1752132853250','81edd8a8-c5c7-4f16-ab71-0efedbe3aff7',1684641600,1687320000,'paid',100.0,'Imported from CSV: 2023-05-21-hoosier.csv','1ca66210-7d70-43d1-b01b-07004f566ac8',1752132902,1752133427,0.0,'20ef93d6-b1c4-4f9a-b1c1-e62423770f6b');
-- Data for beenvoice_invoice_item (343 records)
INSERT INTO beenvoice_invoice_item VALUES('9b237b0e-d47e-47d3-9351-777d10c84d38','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64731153600,'Virtual',1.5,20.0,30.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('8fb85a95-50f9-4375-86d2-5e0e334d87ce','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64731326400,'In person',3.0,20.0,60.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('d9f841ae-4c70-4b3d-ba6a-befec3e07693','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64732363200,'In person',2.0,20.0,40.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('fd91ea66-4c98-468d-a1ae-1d6715c028c2','76d570fe-bfec-47bd-a7fa-b4ee8133c78e',64732536000,'In person',4.5,20.0,90.0,1752132158,0);
INSERT INTO beenvoice_invoice_item VALUES('bb1b3ccc-35be-47b9-a328-386d7fdc0260','61c3d28c-5031-4372-86e3-5bf895411046',64733054400,'In person',2.5,20.0,50.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('33de41fb-3117-4bef-8ced-b9955538f920','61c3d28c-5031-4372-86e3-5bf895411046',64733140800,'In person',5.5,20.0,110.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('22f8db2c-4d80-4847-8927-7fcce399627e','61c3d28c-5031-4372-86e3-5bf895411046',64733572800,'In person',3.0,20.0,60.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('52d4126f-3e1b-4f11-a1cd-c14f64ef8785','61c3d28c-5031-4372-86e3-5bf895411046',64733745600,'Race day (flat rate)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('b9588fcb-2081-44f4-a167-2b51567d89a1','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1621051200,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('fd024a5f-1bf3-4a08-9fb1-fd39502158eb','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1621656000,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('63e0e171-d1f9-43a7-a465-d883b4996b53','57fcd73a-0876-4e91-9856-0f9c9695fcd1',1622865600,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('af8b2c9d-147b-49b4-b0a7-0a98ba63abee','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1620619200,'Fix routers',3.0,20.0,60.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('f9f4712d-9096-4322-978f-3fdff9591939','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1620792000,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('dc0dc83c-093a-42c1-9c8e-b658f5cac7ef','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1621396800,'Race Day (Fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('c1d379b1-70ea-44c4-a3cd-d4e1f1510722','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1622520000,'RDP Login Configuration',2.5,20.0,50.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('1212495d-3d81-47ed-ad57-2f938330a95b','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1338696000,'Virtual Database Install/Setup',5.0,20.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('dfd60b61-908c-4a8e-b768-c471cbf1699a','555b1f8c-7a57-427f-bea8-ee4a3f2a2bf2',1623297600,'Race Day (fixed)',1.0,100.0,100.0,1752132159,0);
INSERT INTO beenvoice_invoice_item VALUES('c79fc31f-2abb-4b4e-968b-8ced90992bfb','4fb5d8be-2588-4187-955d-e7643b08619f',1627617600,'Office Internet/3Play Configuration',4.0,20.0,80.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('286c3631-0a36-4177-83e2-e041d3e5e198','4fb5d8be-2588-4187-955d-e7643b08619f',1627704000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('a0599f94-dcbb-4ff7-8f69-f685b200d702','4fb5d8be-2588-4187-955d-e7643b08619f',1628308800,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('96f1bee1-1117-4fb8-a40a-4fcd485d6528','f48104da-1baa-4a70-9d0c-c03f4017f60d',1628740800,'Stream Deck/Tower Server',2.5,20.0,50.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('41fcea00-259c-433c-8744-1da4297ee261','f48104da-1baa-4a70-9d0c-c03f4017f60d',1628913600,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('a5a677ea-1c26-4c93-bee5-4e7193d8fc54','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629432000,'Office Server Ransomware/Data Recovery',5.0,20.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('fc9e7932-aae0-4611-8dfd-439632e02efe','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629518400,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('14d8f2e4-79a1-4f52-80cb-495422c2ff6c','f48104da-1baa-4a70-9d0c-c03f4017f60d',1629864000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('848765c1-2f93-4fe0-bd54-83a8ed6e028b','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1630728000,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('28b59943-9beb-4c64-bf94-f10729ef55e9','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631332800,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('ef1b5cc8-046e-4720-9126-365bf2011cef','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631419200,'Office Server Data Migration (Online)',2.0,20.0,40.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('07d42569-5d78-4ddc-9146-07c68df081f0','d0c7c941-d0e4-4d55-b1e5-10ed71f1c9f5',1631937600,'Race Day (Fixed)',1.0,100.0,100.0,1752132160,0);
INSERT INTO beenvoice_invoice_item VALUES('f87f4371-4d88-461b-9e20-218841842abd','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1635739200,'IT Move server/Vmix/Backups',2.0,20.0,40.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('fb6b4cf4-b569-42ac-ba14-53e242d07560','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1635825600,'Prep In Car Cameras',3.0,20.0,60.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('f31d6dab-9af3-476c-a272-6e53c3e81a51','6c4314c7-7bc7-4d8a-9513-59a1ebcfd890',1636520400,'Race Day,Islip 300',1.0,100.0,100.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('0a800d16-bf03-4139-93f6-872a455fbd57','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1649390400,'Hoosier Tire Scanning',3.0,20.0,60.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('91c2086d-590a-45ff-8857-006964144c6c','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1649736000,'SSD Migration/Data Backup',4.0,20.0,80.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('d705c999-1112-4215-97c8-81888281a27d','b018eaca-b4b1-4c96-8e40-2a1ab5211e48',1650513600,'Roster Numbers/Data Migration',5.5,20.0,110.0,1752132161,0);
INSERT INTO beenvoice_invoice_item VALUES('1cf32daa-b16e-47a9-8d17-3bb65e8bf654','a0da2a05-5681-46fd-b988-235ec24971e2',1651636800,'Laptop setup/Facebook stream',5.0,20.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('87cee56a-7582-4015-9183-7b917b685b7a','a0da2a05-5681-46fd-b988-235ec24971e2',1652500800,'Race Day',1.0,100.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('adf5caac-3381-4811-aa9a-fe64c6c0ad20','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652500800,'Tire Sales (Hoosier)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('d278998a-ed4e-47bd-8915-35124d8bc27f','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652846400,'Raceway CMS Development',6.0,20.0,120.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('694aaa24-b883-4aa1-b365-3e3ded6e9c4f','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1652932800,'Raceway CMS Development',5.0,20.0,100.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9f5571af-e79e-4254-a370-deb25f16f06c','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653019200,'Raceway CMS Development',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('d9f1fcca-a6f1-4f4e-a6ba-52ea102db90a','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653105600,'Race day (RR)',6.0,20.0,120.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('ebb93ccc-4a9d-4d6f-8584-f044377fdc00','713a368a-f7de-4de8-95dd-2a4a2d626fa1',1653105600,'Tire Sales (Hoosier)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('a4f27be7-68ec-492a-b127-21fa207bde52','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653192000,'Raceway CMS Development',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9749ad17-0e9b-4682-8011-aee73425354b','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653278400,'Raceway CMS Development',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('29fadf5f-a919-420c-a4a7-778d62b770f9','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653364800,'Raceway CMS Development',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('b7f57cc5-ecea-49e3-bb42-15e90dfba1df','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653537600,'Raceway CMS Development',1.0,20.0,20.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('a8380c9d-0444-4afe-b820-9597a871a903','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1653624000,'Generate points tables on site/tire LAN',4.0,20.0,80.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('fd72e334-4e6a-4462-82a5-cc5a8d3ecda0','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654056000,'Press Release Publish',1.0,20.0,20.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('7fc11bdd-b740-4c2a-9cf1-2c3bab092f77','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654315200,'Race Day (RR)',3.0,20.0,60.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('23fbcc77-d0d2-4d0e-90d5-e2f9cab790f7','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654488000,'PR Archive Integration/Points Update',2.0,20.0,40.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('70be501b-a496-4f40-aebc-a4521fbcf4ba','fac3b7e2-9816-459c-960e-ac520b3f2cd5',1654574400,'Press Release Website Deployment',2.0,20.0,40.0,1752132162,0);
INSERT INTO beenvoice_invoice_item VALUES('9bf35628-6046-44e2-a24f-681ea5bf7bb9','8704d2fe-8972-4dae-8062-2f5b81e14493',1654747200,'Raceway CMS Development',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('abea397c-2ea5-4788-9560-42ea0d508bce','8704d2fe-8972-4dae-8062-2f5b81e14493',1654833600,'Raceway CMS Development',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('99590ee9-7e6a-40ec-8925-b135457ba01e','8704d2fe-8972-4dae-8062-2f5b81e14493',1655092800,'TRMM Installation/Script Writing',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('e1f534be-9fe8-42f1-8ef4-bc4073d8ce2b','8704d2fe-8972-4dae-8062-2f5b81e14493',1655265600,'PC Updates',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('7e619cdf-af99-4c58-be0e-227324710e4e','8704d2fe-8972-4dae-8062-2f5b81e14493',1655352000,'3Play Remote Access Setup',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('7fd71054-7626-4f53-94e9-5fc4006ca3c4','8704d2fe-8972-4dae-8062-2f5b81e14493',1655524800,'Race Day',8.0,20.0,160.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c9d74e45-d270-45b4-9332-25db44c9d6d1','8704d2fe-8972-4dae-8062-2f5b81e14493',1655697600,'Move and reassign printer',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('ae2ef12b-43b2-454e-90bf-d8b150f89278','8704d2fe-8972-4dae-8062-2f5b81e14493',1655870400,'Website updates/PR Logic',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('f96d139f-7f40-4a25-89cc-05510c782a7d','8704d2fe-8972-4dae-8062-2f5b81e14493',1656302400,'Website updates',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('e4c9c5ac-ee0d-490f-9f84-c542ad4b7c5c','8704d2fe-8972-4dae-8062-2f5b81e14493',1656475200,'Website updates/schedule/press-release',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('fb35940e-4199-4de8-a163-5ffac86ab0c4','babfc847-b37d-44f2-91a9-4251691c11b4',1658376000,'Server updates and TMM',5.0,20.0,100.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('8f4a5ca2-88d9-403e-9098-6b398d4be218','babfc847-b37d-44f2-91a9-4251691c11b4',1658548800,'Race Day (RR)',9.0,20.0,180.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('9107c846-b7b9-4d37-aecf-8b7cbc6cfc70','babfc847-b37d-44f2-91a9-4251691c11b4',1658721600,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('51e881b1-0e7f-4bcd-87da-3512e2345337','babfc847-b37d-44f2-91a9-4251691c11b4',1658808000,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('707b9108-81ea-4af6-aa2f-0de09220a1a8','babfc847-b37d-44f2-91a9-4251691c11b4',1658894400,'CMS Development (remote)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('9d4c904c-2421-442f-9b45-09a330de83a4','babfc847-b37d-44f2-91a9-4251691c11b4',1658980800,'CMS Development (in person)',5.0,20.0,100.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('5685ea85-1190-45b5-bc0b-65d3a0ae37f5','babfc847-b37d-44f2-91a9-4251691c11b4',1659153600,'Race Day (Hoosier)',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1d14d0de-642f-4266-a466-30ba7773b55f','babfc847-b37d-44f2-91a9-4251691c11b4',1659153600,'Race Day (RR) / Drone photography',6.0,20.0,120.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('93dfc1f6-e3d7-4c5a-8684-32534458bae9','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1660017600,'Update points, change prices, fix pdf display',1.0,20.0,20.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('24f10b26-5ccb-4217-ae89-11d601b16f67','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1660104000,'Add Penalty Reports to CMS',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('f0f74076-daed-4e92-9693-ede280cc3e19','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1672203600,'Server drive replacement/data recovery',4.0,20.0,80.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('40e280f9-1a60-4765-9d3c-bbd6f7546e0a','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643605200,'Add PR support to CMS',4.5,20.0,90.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c3160122-ac8c-4e1e-9f12-be70dae50d38','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643691600,'Deploy PR update to CMS backend',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1c23838c-134e-486a-8e94-7d2d085ce4b2','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643778000,'Update database schema to support PR',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('c6c1d55b-0895-4e4c-9b48-2de18dd4b3a8','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1643778000,'Patch riverheadraceway.com frontend for PR',2.5,20.0,50.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('1ce0b765-4330-454e-b339-679d3a61560c','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644123600,'Begin new schema for schedule upload',2.0,20.0,40.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('41f3b4a2-d0ac-4813-8c97-353151735140','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644642000,'Prototype rules upload page',3.0,20.0,60.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('296f2767-f2a1-48ee-afdd-7d9e5a5d4373','89f677fb-ca0f-4d43-9547-d4da77f0f0ba',1644814800,'Rules CMS page backend dev',1.0,20.0,20.0,1752132163,0);
INSERT INTO beenvoice_invoice_item VALUES('6df444f9-9013-4ba4-889f-288687bf40cd','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679025600,'Website fixes, Orbits Suite Update (5.9)',4.0,20.0,80.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('b8b7d422-8eb4-4550-8b8c-75d1eebb606c','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679284800,'Backblaze B2 Backup setup for VMs/web',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('0a871404-dd23-47ae-9b53-4db1762424db','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679371200,'Install and provision Active Directory SRV',4.5,20.0,90.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('e6ae1b2c-e842-431d-b2d1-fbe46f0d29d5','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679457600,'Update BackBlaze configurations',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f540b78f-10a7-4f10-9409-10f54eff831e','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679457600,'Website edits',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('7dd05c87-6a3e-40e6-836a-63dd7e22d52c','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679544000,'Remove policies from website',0.5,20.0,10.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('5dd99069-2388-4daa-b304-a5e6f000bbaa','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679544000,'Add dynamic roster to website',3.5,20.0,70.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('64f37c00-5f5f-49d3-85bc-786d083abc01','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679630400,'Update handicapping rules, modify reserved',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f7845e1a-cfaf-4b97-9404-985c578cd35d','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1679889600,'Update CMOD rules, separate bandos',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('1967e5b2-98ae-493b-ba34-b28c81ebeed9','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681272000,'Separate/ configure user accounts for FM',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('623d2cee-7c09-4626-bff7-16b4af75a3ac','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681704000,'Generate and email RDP deployments',1.0,20.0,20.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('9a5cac21-e2f8-4028-b90d-2f1d1701abb6','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1681876800,'Generate roster CSV and convert to FM',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('dda3b050-8ad8-45fb-bee5-48bc2e94c469','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1682395200,'Troubleshoot FM access',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('2e562992-b42e-4d1d-99eb-ff354b2194d8','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1682568000,'Generate login certificates/install FM server',2.0,20.0,40.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8542b97d-a5e5-4a95-b143-9677c9ca2c09','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1683000000,'Reset RDP cache on Vmix PC/initialize',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('54ba18de-ae79-4f36-a5f7-5e112e7033fe','2a07bf2e-1923-4b4b-aba9-14c507a2f2c4',1683086400,'Unify user accounts on AD for FM',2.5,20.0,50.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6446d0af-4267-42fb-b929-18705adf748a','0b057a65-fe7d-4495-8756-4dd61f6895e1',1683777600,'On-site- printer and system update/config',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('967b4092-caf5-4fe9-94ae-9ad05d021abd','0b057a65-fe7d-4495-8756-4dd61f6895e1',1683950400,'Race day',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d13204e9-14e8-4cf6-af8d-0d554f865897','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684123200,'FM Maintenance/Web development',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('e3e9ce1b-ed84-47e3-822f-f844b7aa0484','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684209600,'PointsSplitter Script (Remote)',1.5,20.0,30.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('eb535a1b-315f-4457-b742-72d01419b2cd','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684296000,'vMix,New ticker and sponsors',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8c318cea-7d7d-4ec5-a6df-63b46e1d36be','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684382400,'Web Development (Remote)',4.0,20.0,80.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6def29d4-4511-4705-b963-29717f881a7a','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684468800,'MyRacePass/Website',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('2a3a5028-d561-43ac-af77-2a2af562b145','0b057a65-fe7d-4495-8756-4dd61f6895e1',1684641600,'Race day',5.0,20.0,100.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d4cba322-8a53-4f72-9e10-16388bbc5e51','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684728000,'MyRacePass Data/FM Server/3Play',4.5,20.0,90.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d2180f5c-685d-4f5e-8a03-b8f6804bbf31','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684814400,'FileMaker Troubleshooting/Maintenance',2.5,20.0,50.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('7feefdb6-1a66-439c-8013-a354d7af4284','f86f4002-6539-44a3-b8c9-ca6689f809c1',1684900800,'New graphics suite',5.5,20.0,110.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d23b7924-9acc-48c4-9d09-067b6f12c0b6','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685073600,'TV Lineups program',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('6bd45327-8f01-4dab-8a92-9b76363ce2d3','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685160000,'Race Day',7.0,20.0,140.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('77d6c533-8d6b-437f-b3bb-7f51dd8f8e5b','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685505600,'PC Maintenance',5.5,20.0,110.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('d83da575-2e45-4dd6-bf39-4f1b553a3d4f','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685592000,'Web Development/CMS backend update',6.0,20.0,120.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('9dfc99e1-87b2-4487-90e2-3d7410bf771f','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685592000,'Equipment Purchase - Black Box',1.0,170.0,170.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('f0ed5f32-fa79-43d8-bbb1-02859b9a9f7d','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685678400,'TV setup and wiring',3.0,20.0,60.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('8e754e3f-eee4-4af7-93c4-1238f32d572c','f86f4002-6539-44a3-b8c9-ca6689f809c1',1685764800,'Race Day',3.0,20.0,60.0,1752132164,0);
INSERT INTO beenvoice_invoice_item VALUES('62ea502c-a52a-462d-93ed-8deb5b8b97af','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1685937600,'Website updates, capture card fix',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('97e3d7c5-a2bc-484b-90ee-8883fafd6842','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686024000,'Web dev',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c2fc47ee-9c96-4b66-9a87-fe52054ab6e7','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686110400,'Website work, Itinerary/Roster fixes',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('08aab334-1ef7-470e-b02b-99b8991cbf78','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686283200,'Quickbooks reinstall/drive copy (on-site)',1.0,20.0,20.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('17957359-ecdd-49be-8a23-257c7bc45e81','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686283200,'Web development',5.0,20.0,100.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('bc1cb3aa-3d66-4b6d-9089-6bdda101503c','ef6a5079-2d65-46b1-8d87-a9ef5c0cb650',1686369600,'Race Day',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('3489a122-5983-461f-bf54-edc0df82a89d','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1687492800,'Reset passwords, hide enduro points',1.5,20.0,30.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('afc213f9-5738-48a2-9be2-91264ee2fd70','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688356800,'On-site website work',6.5,20.0,130.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('5c7c9b42-8da1-4cfe-a683-b2175588d4a0','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688443200,'Remote website work',3.5,20.0,70.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ee858f44-df11-4754-a105-418a0c392f5a','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688443200,'Microsoft Office 2019 ProPlus',1.0,30.0,30.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ddf90623-e1c7-4d77-b215-20e3bdcf057c','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688529600,'On-site website work',6.0,20.0,120.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('62185930-638a-4de1-80f4-cb594af09848','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688702400,'On-site website work',9.0,20.0,180.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c4a6df10-ff6b-475f-9614-3ab87bc891dc','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688788800,'Race Day',9.0,20.0,180.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('6c7cb646-294c-4279-bd78-986b84b99c01','9da8c49f-f6e5-4e5d-8da6-2c1baaf1e5cb',1688875200,'Website work',3.0,20.0,60.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b4575be6-65d8-435d-974f-e3a741500ba4','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1688961600,'On-site website work',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('f2282616-a3f4-4920-9d12-c89251d67468','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689048000,'Remote website work',6.5,20.0,130.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('ede3c1c2-d80e-489a-945d-a61e24e15f1f','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689134400,'Remote website work',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('af24fb33-cde7-4bb8-a0ba-b81a9fb6222c','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689220800,'On-site computer work',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('1e16ad5f-a961-46ed-a58c-4423a830839c','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689220800,'Remote website work',4.0,20.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b575b193-ce8e-415c-8689-6a8fac8e7a1f','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689307200,'On-site computer/website',7.0,20.0,140.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('2f4fd87b-4a2e-4ddb-88c3-770a36bf5640','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689393600,'Race Day',6.0,20.0,120.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('b2525223-cf5e-4a2e-a07c-ba3972f51409','9186435f-2b62-4c58-aa45-c00aeac9c7d6',1689393600,'Acer SB220Q',1.0,80.0,80.0,1752132165,0);
INSERT INTO beenvoice_invoice_item VALUES('c859a866-6487-432d-ad05-cf6bc732c6c6','a722008f-f269-4018-b755-b25cd2c5471a',1658030400,'Website (off-site)',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('284084d7-cb14-40db-8017-99aa6182741f','a722008f-f269-4018-b755-b25cd2c5471a',1658116800,'Website (on-site)',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ea1457ef-79a9-4d36-95e9-98667ab57de4','a722008f-f269-4018-b755-b25cd2c5471a',1658203200,'Website (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('64d111a0-6371-44d5-994d-afd5e47491ca','a722008f-f269-4018-b755-b25cd2c5471a',1658289600,'Move ThinkCentre/Tires',7.0,20.0,140.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('a853c17d-bc3e-45a4-88d0-48ca01631e88','a722008f-f269-4018-b755-b25cd2c5471a',1658376000,'Audience Display and Points',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8118f5c5-9570-4e93-ab07-9262ca30b3bb','a722008f-f269-4018-b755-b25cd2c5471a',1658376000,'Website (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('55120798-2208-48e9-b617-804e595f35e7','a722008f-f269-4018-b755-b25cd2c5471a',1658462400,'Race Day',7.0,20.0,140.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('327193a6-9393-498e-8bb3-caff95069727','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690171200,'Website work and graphics',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8ff3daae-0a76-4df9-b94f-7e1aa954a3aa','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690257600,'Website backend (off-site)',3.5,20.0,70.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('d2684ffe-51ab-4f20-8539-b7d1a1b76f87','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690344000,'Headshots and placeholders',5.5,20.0,110.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('c9bf1a18-b45e-4c5f-abf4-34cf709fe689','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690430400,'Lineups and auth security (off-site)',4.5,20.0,90.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('97cc7b06-caa1-4a75-b1f7-3f95ab0b5e19','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690516800,'Audience display, news editor, prices',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('6d9aeec4-a3de-4faa-be35-feacdb39e350','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690603200,'Price editor, begin database migration',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('eab32b2d-9edb-4a71-ad92-11d872857be9','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690689600,'Database migration, match up 2022 reg',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('0c133b57-d722-47a0-b390-c7ada5e555d9','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690776000,'Begin express registration (auto) (on-site)',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('3e8072e3-d462-492c-a32e-5bafa12ac66d','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690776000,'Finalize express registration (off-site)',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('1ef0a686-5c33-4bce-892e-b72cb4f6528a','ed3cf514-1438-4ee0-8e72-3f47c0f9aa15',1690862400,'Champion bios, rework points for new DB',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('9f75cdeb-4833-4325-abdf-f392c8be311b','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1690948800,'Race Day',8.5,20.0,170.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('8ca5bc48-ae7c-458d-9ae5-da54edb580bd','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691035200,'Website hotfixes',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('bf316f01-6d97-4887-bbb4-7f6bc04e1075','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691121600,'Tire/Office swap, website final touches',5.5,20.0,110.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('37f86e75-a336-4f7b-ae0c-345dd584d1a1','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691208000,'Race Day/Website publish',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ddedea49-53f5-4d91-913d-48156ac2b4cc','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691380800,'Database fixes (on-site)',3.5,20.0,70.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('a1315055-0a28-4a70-9701-433201cd4870','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691380800,'Draft wall of champions page',3.0,20.0,60.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('4e858515-b73a-411e-90ca-605b396c7d9c','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691553600,'Webcam/Wall of champions (on-site)',6.0,20.0,120.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('e8121138-1871-41f0-8904-0f43ce5e4690','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691553600,'Wall of Champions Finalize/Publish',4.0,20.0,80.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('480dca64-e20f-43a9-8b6c-77acd8902f3d','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691640000,'Migrate to managed DB/Hall of Fame',7.5,20.0,150.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('34d04265-a18f-4ec7-9031-7141fe411c28','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691726400,'WiFi install, laptop setup, Add results to site',4.5,20.0,90.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('ce8e3f34-5c0b-4e51-80f9-13ef76a05e74','c7e84ee9-ae1e-4f31-b120-6cc7e02b0442',1691812800,'Race Day',5.0,20.0,100.0,1752132166,0);
INSERT INTO beenvoice_invoice_item VALUES('771c15eb-0062-43ca-9a72-cc76069cd02a','e18f8253-59a5-45ab-9070-8397930c8e12',1692676800,'Points repair',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('dfe1d780-2ce9-4aa3-bafe-692dfc5e4e3f','e18f8253-59a5-45ab-9070-8397930c8e12',1692936000,'Add JuiceBox division to site',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('d925981d-a0cc-4902-83e4-72cea6400014','e18f8253-59a5-45ab-9070-8397930c8e12',1693022400,'Prep site for ISP300 ticket/reg sale',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('7ac101d2-21a3-423b-ae3e-d7e796cad4cb','e18f8253-59a5-45ab-9070-8397930c8e12',1693972800,'Bring up old database site',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('30633312-1e3a-4bd6-9e36-d56a0c455a7c','e18f8253-59a5-45ab-9070-8397930c8e12',1694750400,'Fix registration car check',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('88f0b570-6406-42c8-adcb-fedd96bcbd1f','e18f8253-59a5-45ab-9070-8397930c8e12',1694836800,'Implement season ID system',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('cbc4efa9-f68e-44f9-b535-eee608c54a9e','e18f8253-59a5-45ab-9070-8397930c8e12',1695268800,'Update website content manager',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e0e20b6f-dcd0-442c-a6c1-108c8a2d4c44','e18f8253-59a5-45ab-9070-8397930c8e12',1695355200,'Add toggle to event visibility, update events',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e8a37197-cf61-4e50-a6ef-f4bc16caf583','e18f8253-59a5-45ab-9070-8397930c8e12',1696305600,'Design/implement BCA month graphics',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('53e884d4-0e44-47ae-9de1-a08e502166d8','e18f8253-59a5-45ab-9070-8397930c8e12',1696392000,'Create special event season/reg',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('d275522f-1659-473a-badc-70abe80aeb07','e18f8253-59a5-45ab-9070-8397930c8e12',1696478400,'Special event roster viewer',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('ccc37a05-5b94-4e8b-a804-167d7d86664e','e18f8253-59a5-45ab-9070-8397930c8e12',1696564800,'Add fee/payment process to special events',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('920b3a78-9957-4c32-b5ed-df46c297e5fc','e18f8253-59a5-45ab-9070-8397930c8e12',1696910400,'Email update (hide personal data from all)',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('85ff6150-7947-41d8-b408-1a816aa0fc76','e18f8253-59a5-45ab-9070-8397930c8e12',1697601600,'Update internal roster viewer for full data',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('8d4d28e2-db87-4a36-891c-2cee4b161bc9','e18f8253-59a5-45ab-9070-8397930c8e12',1697688000,'DB sanitization, prep for export 1099',1.5,20.0,30.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('0d68af50-8887-467a-b1d0-1071e2c479e3','e18f8253-59a5-45ab-9070-8397930c8e12',1698206400,'Add special event roster viewer to site',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('c84b3e48-f1b6-4199-bcdd-6f8685b2774f','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1698811200,'SE roster, change theme, update events.',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('e9a56413-22c6-4736-8e60-d510bb2ae953','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1698897600,'SE roster visibility, live DB detection',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('aa720f49-b51a-437b-a413-4a9f6a4f9544','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699070400,'CMS RosterView Update',7.0,20.0,140.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('bf2c1ba6-8d19-4280-84ce-8173b863c23c','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699246800,'CMS Backend Redesign (OOP)',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('704f9d4a-27d6-4b25-af11-43ac8211959b','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1699333200,'Various DB/Roster updates/exports',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('2bc45810-c0bb-4150-9191-e27efa42d7c4','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700197200,'Shopify Website Design/Setup',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('208cea71-e378-494d-bcff-92c19ead51b7','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700370000,'Special Event Mail Merge',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('a5f209a2-a65e-4e29-a137-4381bb477327','f39a6380-e1c0-4a28-b25e-f960e40ebbdc',1700456400,'Special Event Envelope Automation',1.0,20.0,20.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('95d8a07d-a5c2-4453-9795-c35cc7fc82b3','352863b6-4bcd-4060-9aee-7a1493381646',1701752400,'Compress all images for quicker site load',2.5,20.0,50.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('02d59723-bdd7-4bba-ba74-adfa0cfc7a16','352863b6-4bcd-4060-9aee-7a1493381646',1701838800,'Begin banquet registration',3.5,20.0,70.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('c58588d4-ac5b-424b-98ad-340157190c5e','352863b6-4bcd-4060-9aee-7a1493381646',1702357200,'Banquet registration database setup',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('407e13d3-4e35-4ec4-a9a0-95c0916193a0','352863b6-4bcd-4060-9aee-7a1493381646',1702443600,'Banquet reg stripe price generation',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('093d4237-83de-4d7f-9e5c-42a719726a03','352863b6-4bcd-4060-9aee-7a1493381646',1702616400,'Online store theming/UI',3.5,20.0,70.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('eaad59e0-1324-4a00-b443-d614fd56a227','352863b6-4bcd-4060-9aee-7a1493381646',1702702800,'Online store pricing/payment',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('36228c80-bc0a-4940-b088-4904b17899e7','352863b6-4bcd-4060-9aee-7a1493381646',1703566800,'Finalize banquet registration',5.5,20.0,110.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('a694d9db-b50c-4863-b552-c80b19f53222','352863b6-4bcd-4060-9aee-7a1493381646',1703653200,'Update champions and win tallys',2.0,20.0,40.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('22612fae-9421-40d9-900e-643638ca7531','352863b6-4bcd-4060-9aee-7a1493381646',1703826000,'Show prev rosters, add announcements',4.5,20.0,90.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('b39974ea-cd78-4271-b6f0-60c9b8c4911c','352863b6-4bcd-4060-9aee-7a1493381646',1703912400,'CMS banquet roster visibility',4.0,20.0,80.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('06bc2406-bbbe-4daa-96d7-d80151aa41e0','352863b6-4bcd-4060-9aee-7a1493381646',1704171600,'Hide registration for fixes, refund users',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('1adee95a-f05a-4f2c-b648-ee1af13ed1ff','352863b6-4bcd-4060-9aee-7a1493381646',1704517200,'Convert to store-pay-update for 2024 reg',3.0,20.0,60.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('066c172c-debd-4227-bbc1-0e4eb8d4d74e','352863b6-4bcd-4060-9aee-7a1493381646',1704603600,'Finalize and publish 2024 registration',5.0,20.0,100.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('3ae082a5-3e30-401c-8757-29306ae32dae','352863b6-4bcd-4060-9aee-7a1493381646',1704776400,'New events editor, disable letters for 2024',7.0,20.0,140.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('2b5c2d59-4611-4f69-8859-3f7e7d3b294e','352863b6-4bcd-4060-9aee-7a1493381646',1704862800,'Rules uploader',8.0,20.0,160.0,1752132167,0);
INSERT INTO beenvoice_invoice_item VALUES('05304002-9b6c-423b-bbee-4637d67041a5','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1704949200,'In-person Track Day',7.5,20.0,150.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('fd6e3b70-9198-4aa2-be41-f2186bfeb52a','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705035600,'Banquet export and mail merge',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('4f662168-ed58-4fa5-99ae-d79eeeae201e','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705208400,'Number reservations',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('90373154-537e-43a0-82a8-fcc036514461','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705381200,'Division page hotfix',0.5,20.0,10.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('67fc6ed2-e430-4e28-90dc-c40bd7c2e3b4','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705467600,'Auto display driver registrations',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6d680a14-f864-4047-8c3b-ff6afcdaf10c','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705640400,'Shopify Finances',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('c7ca7f6d-bba8-436f-9ecc-13b7e67993c7','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705813200,'Banquet Mail Merge pt.2',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('18154b9a-0377-48ee-b3b6-64e0aafa45ff','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1705813200,'Banquet ticket close/clean up',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6f2849cc-65d9-44fe-8b12-82c551fa71a2','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1706245200,'Take down schedule, fix event publisher',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f0a627d2-5f3a-4a9c-ab54-f7da5a304b00','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1706590800,'Permissions, sponsor links',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f0349c88-43ff-4dba-9cfe-5940713b1612','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707022800,'Begin new roster viewer/editor',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('fcafdfbb-6f33-44d0-8044-4450b772b061','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707109200,'Roster editor UI/Tables',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f3b99670-1f60-4e87-bb80-95170ddd784f','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707368400,'Roster editor,change participants/autofill',4.5,20.0,90.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('10134ca6-0e8a-4c41-a91b-13945a12a4cb','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707454800,'Roster editor,Auto tax form generation',6.5,20.0,130.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('a3b9aaec-8ba1-49fc-b1a9-7506fd84460a','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707627600,'Update CMS navigation',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('52460b23-e519-4fc6-ac89-46576070f9f3','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1707714000,'CMS User Manager/Perms editor',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('c80150ba-34fb-4b9b-a9a5-78024e7b5e40','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1708837200,'NASCAR Reg Link, general typos',0.5,20.0,10.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('4c88ca6d-482e-489c-9da9-16fa2cc8bd00','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1710129600,'Track day',4.0,20.0,80.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('5c8995f6-b191-4ba6-b129-0537785e156e','dc0e0595-07a8-471b-8f7b-23cd13c0b8c1',1710216000,'Event page custom links',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('0b2c3bc2-be20-4c16-b384-9d5bd1e2e693','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710388800,'Track Day',3.5,20.0,70.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('78553d71-aa77-4791-8ec1-0d2b43973308','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710475200,'Remote Onedrive Support',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('e7902386-266d-4b8a-85ce-47851e181d02','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1710907200,'Data collection/analysis for site',2.5,20.0,50.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('23c59227-54d9-43ad-9b34-a554b52ba74f','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711339200,'Driver 1099/W-9 generation update',5.0,20.0,100.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('d18e53f0-ead0-4b56-b56c-be2b7671e7ea','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711425600,'Itinerary search/export',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('51f1ebdd-f68b-40c6-83b7-d3b413882360','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711512000,'Itinerary resend, Reg data export/merge',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f4145173-a276-458c-a8d3-c8b94b5c4cf5','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711598400,'Fix itinerary missing from website',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('6dd9e3c8-7def-48a2-840b-a72de7e1c753','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1711944000,'Roster/Itinerary updates',2.0,20.0,40.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('61b692a2-8c63-4061-9f35-30844a2cedd1','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712548800,'Roster download link',1.0,20.0,20.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('a571bef7-b402-4316-b4db-209679d67fed','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712721600,'Roster phone number export patch',2.5,20.0,50.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('38290567-dc1f-420c-8a74-1fda829e218d','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1712808000,'Stripe support contact/ticket',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('5835c23b-3872-45e8-b7fc-1e9884313a26','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1713153600,'Credit card charge match with stripe',1.5,20.0,30.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('f7e8504f-a95c-4921-9859-6f5c0687b1ad','cf6ea6c8-c485-4a01-aa12-f68306ef426a',1714017600,'Exit cleanup/account reassignment',3.0,20.0,60.0,1752132168,0);
INSERT INTO beenvoice_invoice_item VALUES('02602261-24d0-4546-88da-ff9fb14c3eed','1942364d-df4e-4175-8210-dbc202ca1038',1733979600,'Begin racehub-next development',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('c6334fbb-6892-4760-a61c-5cdc04921c72','1942364d-df4e-4175-8210-dbc202ca1038',1734066000,'Migrate basic features, authentication',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('33957386-0976-4800-a01d-2a5977e8df2a','1942364d-df4e-4175-8210-dbc202ca1038',1734498000,'Logistics planning and roadmap',1.0,25.0,25.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('dacda1bb-a445-4cdc-bdc5-db3bd1f48de1','1942364d-df4e-4175-8210-dbc202ca1038',1734670800,'Change racehub-php season, begin DB',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d5bddee3-1892-4c7b-bab9-50598fcf7d83','1942364d-df4e-4175-8210-dbc202ca1038',1734757200,'Events page integration, rich homepage',5.5,25.0,137.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('3d80aba0-53d9-40d9-a163-5dc6aff36320','1942364d-df4e-4175-8210-dbc202ca1038',1734930000,'Create news page, optimize loading flow',5.0,25.0,125.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('f21fba61-87c1-4426-af99-450e42c193f5','1942364d-df4e-4175-8210-dbc202ca1038',1735016400,'Begin DigitalOcean provisioning/deploy',2.5,25.0,62.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('86ec004d-cc98-4c37-9943-1a7f60170d69','1942364d-df4e-4175-8210-dbc202ca1038',1735189200,'Deploy app/DB, news page optimizations',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('ab7cc962-5024-48e6-979a-885ccf6a7194','1942364d-df4e-4175-8210-dbc202ca1038',1735275600,'Fix deployment issues, integrate DO App',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('08d9e276-6dcf-4e50-9de5-dd13b580fe6f','1942364d-df4e-4175-8210-dbc202ca1038',1735362000,'Add image compression, content delivery',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('6a243898-3ef9-47f0-9008-9e3fca0a1c33','1942364d-df4e-4175-8210-dbc202ca1038',1735448400,'Announcements, Promo, Sponsors CMS',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('23059eac-34fb-44d2-9c36-1e10e387167d','1942364d-df4e-4175-8210-dbc202ca1038',1704171600,'Begin competitors page',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('9758e516-7655-4410-9f12-b069326ff3e2','1942364d-df4e-4175-8210-dbc202ca1038',1704258000,'Migrate APIs to tRPC for data security',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('628da274-f479-4603-bde2-9556795a6d4d','1942364d-df4e-4175-8210-dbc202ca1038',1704344400,'Recreate articles CMS for rich text',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('12a7c5ef-4029-410f-b176-a52966015698','1942364d-df4e-4175-8210-dbc202ca1038',1704430800,'Migrate announcements editor, add raindate',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('bea4d148-602b-4cc6-a1a9-4b9a7717c050','1942364d-df4e-4175-8210-dbc202ca1038',1704517200,'Discuss and plan out site scope (In-person)',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d286c494-64c4-4eaf-9a8d-5ad681b4413b','1942364d-df4e-4175-8210-dbc202ca1038',1704517200,'Implement reports, rules, and champs',6.0,25.0,150.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d43cedf4-a854-44ef-8853-725369212bd6','1942364d-df4e-4175-8210-dbc202ca1038',1704603600,'Add CMS authentication, route protection',6.5,25.0,162.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('fb557beb-9912-4e63-b883-8ff74451062b','1942364d-df4e-4175-8210-dbc202ca1038',1704690000,'Clean up deployment, fix UI/display bugs',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('1383248a-2301-4df4-985d-042cd44c1c49','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736398800,'Correct rain date and sponsor editor saves',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('dff81591-7781-45a2-b7b4-2e729c15048b','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736485200,'Fix bugs with article editor and images',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('01f8a30d-e04e-4ccc-ad18-da918e677ff9','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736571600,'Add upload event image/compress for load',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d2f51448-c17c-4dc1-bfb3-09f7af3f9d3a','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736744400,'Work w/ hotlap to get registration roster',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('70f9a81a-a4c6-4c78-b80e-0b5a6b0123a0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736830800,'Add user management w/ email pwd reset',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b06d988c-abb5-40a7-baad-f35878cf11e9','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1736917200,'Finalize code for public, deploy site, bkp old',6.5,25.0,162.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b6ef7b4b-f43a-472d-abbb-49031e268e88','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737003600,'Add analytics for page views and clicks',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('4081c2cd-2af2-4283-9e37-5992557666c7','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737090000,'Track System Setup/Shopify (In-person)',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('29a894b6-46c4-4a01-a7c8-4ebe0fc9c0cd','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737176400,'Begin real-time banquet voting system',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b8a10fea-3e9b-4885-ae1c-ef222a6584e4','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737435600,'extract/export W9 information for 2024',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d78e4072-375c-41e2-8a81-69b7380b9d30','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737522000,'Implement 2024 roster for voting',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('735d00db-dd71-48a2-81dc-d4ab34dc3733','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737608400,'Test and complete deployment of voting',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('aa9f9359-04a1-4b47-8515-dec844564502','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737694800,'Push and enable banquet voting, fix bugs',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('881a19bf-b655-407d-9a52-1639ce13c5fe','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737781200,'Remove banquet voting, show points tables',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('c07aed16-5c22-4fde-9476-b8a8a7485572','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737867600,'CMS Reconfiguration for SS and MS class',2.5,25.0,62.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('726427ac-a5f0-4c05-9efd-0402fa6e30f0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1737954000,'Competitors and division page redesign',5.0,25.0,125.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('874a1159-df26-4851-8dc5-d34509b25e77','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738040400,'Browser conflict tests and fixes',3.5,25.0,87.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('b0fb99d3-9c32-4729-89ce-7aab0ba98256','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738299600,'Rules CMS Editor upload and edit repair',4.5,25.0,112.5,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('33cda6b9-cdc6-4211-a52f-a6aa9badaf2f','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738558800,'Migrate backup from BB to DO, sys updates',4.0,25.0,100.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('d1dfaa3d-c880-47c4-b2a8-5e1c61b72ae0','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738645200,'Create and verify backup scripts',3.0,25.0,75.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('21627b66-05b7-472e-8df6-ddc37554bf3b','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738731600,'Optimize devenv to use locally hosted S3/DB',2.0,25.0,50.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('271561b0-b8af-4603-aa43-49ba87bc4da6','547569b8-2f7c-486b-a4f1-2a7b80aa904a',1738904400,'Verify integrity of backup change chunks',1.0,25.0,25.0,1752132169,0);
INSERT INTO beenvoice_invoice_item VALUES('f4d05559-46e7-46da-8cf0-00606e63fb49','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1741150800,'Limit event display, update event layout',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('b7a66b38-1628-46cd-be21-0d9d0f7c105a','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1741579200,'Work w/ cloudflare to inc. file size limit',1.0,25.0,25.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8a98405f-ff6b-4e64-83aa-25cf2ad0e3cb','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742097600,'update/fix article saving/loading process',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('80c17c33-bc48-44e3-b358-73dc7df0b63e','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742184000,'update/fix rule saving/loading process',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('d8f3066a-ea93-4221-8d9e-1921fb31d006','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742270400,'patch Next.JS emerg. security vulnerability',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('41c658b0-8020-4471-9e8d-e0f67108c9a9','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742356800,'Update server headers to use new limit',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('933bad4e-f7da-452c-b8f5-be6d631cbe23','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742702400,'Add PDF export of events/rules on demand',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('d204cb6a-be0e-4ee7-8c46-a7f532c7a291','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742788800,'Add file caching to save $ on server usage',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('7e172229-4a68-482c-b429-326e228d185e','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742875200,'Add video upload, begin driver testimonial',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('19c8f1e4-e676-40c2-ba1a-c370c2491af8','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1742961600,'Disable points section, prep for new points',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('1c26eeb4-22de-47bd-a170-d003fda1a213','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1743048000,'Finalize testimonial, update/enable points',5.0,25.0,125.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c5aaf396-c27b-44ac-b141-c69872d87a4d','bd64542e-c576-4dd7-b0d4-f4d6077aef25',1743393600,'Retrieve and display previous itineraries',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('df365fb2-9d75-4589-83e4-48969e62df5d','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746244800,'Lineups upload interface finalized/pushed',4.0,25.0,100.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('0a35f2d9-15b1-4d82-9ba1-df27f0024f6f','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746331200,'Lineup audience display',3.75,25.0,93.75,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6d6502d2-0f0d-4521-8943-4ae78e5bc7d9','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746417600,'Lineup mobile display',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('258f123f-f80b-4920-af38-08bc8d163f5e','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1746849600,'Begin points upload system backend',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c517d481-9741-4283-b74c-e61b500cfd2c','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747108800,'FileMaker points parsing logic',2.0,25.0,50.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('4e5e6815-6b4b-4433-8a52-dafcbcdd7284','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747195200,'Update spectator policy system',1.5,25.0,37.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('fa70e962-0678-4593-b8fd-8abab5a26c6b','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747454400,'Restructure lineup page logic for old phone',3.5,25.0,87.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('9e1878f9-f485-408e-91c9-281b02737d3e','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747540800,'Handle cross time zone errors w/ lineups',2.5,25.0,62.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8effbfe4-1434-4448-b7a6-5ab316fc93f9','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1747800000,'Crate mod points issue fix',1.0,25.0,25.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('12ea8e17-eac6-42b4-aa22-3981003172a5','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748404800,'In person, website/network planning',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c8bd20a4-19d0-47ca-b381-93bd6e5fd2dc','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748491200,'Rain date API integration/management',5.0,25.0,125.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('02ce3809-d900-4f1f-9400-64b225d61339','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748577600,'Begin lineup patches for visibility',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('8f4d886d-8d3d-4a30-bec7-b41ee854f731','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748664000,'Bind rain dates to events, show reschedule',5.5,25.0,137.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6fce0331-9208-408f-8369-4fb4a2fb2fa4','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748750400,'drag and drop lineups, divisions cms update',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('6f49db90-fa25-44ab-9ef4-57c00c9c36c3','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748836800,'home page reordering, QoL improvements',4.5,25.0,112.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c63d9d91-6e0c-48f8-b2f1-a02c4839848c','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748923200,'In person, bulk email system',3.5,25.0,87.5,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('61b3faf1-4edc-4b05-9914-45fa8b49b51f','5a8f214c-8f6d-46e9-949e-1e9e31c40974',1748923200,'Remote, bulk email/delta points',3.0,25.0,75.0,1752132170,0);
INSERT INTO beenvoice_invoice_item VALUES('c236b466-5706-4bad-8324-5219c17dd2f2','06c43197-9685-4116-b83b-1c76840905ab',1652500800,'Replay Operator',10.0,40.0,400.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('772bdeaa-a5a9-4c7e-8a54-d02b6d115e16','a66739ec-fbfe-4871-8388-0b34b2228889',1683777600,'Install and configure tech PCs',2.0,20.0,40.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('d88ffb8e-4c29-4882-8dff-dd2d227b1639','a66739ec-fbfe-4871-8388-0b34b2228889',1683950400,'Tire shack sales/maintenance',2.0,20.0,40.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('66eeb92c-ecf3-46c1-b6f6-6569b90fe598','a66739ec-fbfe-4871-8388-0b34b2228889',1684123200,'Tire program/scanning',1.0,20.0,20.0,1752132902,0);
INSERT INTO beenvoice_invoice_item VALUES('8f00b60d-5dc7-4f19-ad3e-2a51d1c4d296','d6a1da99-d066-4993-b907-1e30a769f107',1743652800,'Correct time-zone errs for non-EST viewers',2.0,25.0,50.0,1752274548,0);
INSERT INTO beenvoice_invoice_item VALUES('3ab632b7-cebc-49a0-8f59-9f39db3c9543','d6a1da99-d066-4993-b907-1e30a769f107',1743912000,'WiFi Setup/Security Updates across sites',2.0,25.0,50.0,1752274548,1);
INSERT INTO beenvoice_invoice_item VALUES('a3cc32fd-a0aa-4986-8ec6-91e6572ed13d','d6a1da99-d066-4993-b907-1e30a769f107',1744084800,'Standardize date handling, data utility upd.',3.5,25.0,87.5,1752274548,2);
INSERT INTO beenvoice_invoice_item VALUES('f5009d53-27e0-4104-bde3-afaeb4c924e7','d6a1da99-d066-4993-b907-1e30a769f107',1744776000,'Rephrase/reorganize home page',2.5,25.0,62.5,1752274548,3);
INSERT INTO beenvoice_invoice_item VALUES('d92fb22e-e0fb-4e82-b2f9-27f8eee5a150','d6a1da99-d066-4993-b907-1e30a769f107',1744862400,'Add ability to remove/submit null timeslots',3.0,25.0,75.0,1752274548,4);
INSERT INTO beenvoice_invoice_item VALUES('a2228d31-0c4c-49f7-ba7c-a09eb4dfe2c5','d6a1da99-d066-4993-b907-1e30a769f107',1744948800,'Hostway email contact investigate/upload',2.5,25.0,62.5,1752274548,5);
INSERT INTO beenvoice_invoice_item VALUES('c5dcc389-3fea-4cfa-98eb-2130016be99a','d6a1da99-d066-4993-b907-1e30a769f107',1745035200,'Re-render live schedule, update deps.',4.0,25.0,100.0,1752274548,6);
INSERT INTO beenvoice_invoice_item VALUES('73386e72-750e-4eb1-83de-e239c66102fe','d6a1da99-d066-4993-b907-1e30a769f107',1745467200,'Add rich text editor to site backend',3.5,25.0,87.5,1752274548,7);
INSERT INTO beenvoice_invoice_item VALUES('87aa98a8-131d-49bb-98fb-0460a8dde4ab','d6a1da99-d066-4993-b907-1e30a769f107',1745553600,'Update mobile view, fix rules pagination',2.0,25.0,50.0,1752274548,8);
INSERT INTO beenvoice_invoice_item VALUES('6fe10405-029e-4164-b918-f521d3830818','d6a1da99-d066-4993-b907-1e30a769f107',1745812800,'Lineups backend port from racehub-php',2.0,25.0,50.0,1752274548,9);
INSERT INTO beenvoice_invoice_item VALUES('62f2594f-0d24-405a-989c-2fcb5392a3e6','d6a1da99-d066-4993-b907-1e30a769f107',1745899200,'Update filemaker, add csv export/import',2.5,25.0,62.5,1752274548,10);
INSERT INTO beenvoice_invoice_item VALUES('208eebce-58e5-4d1a-8088-47a516fe39c9','d6a1da99-d066-4993-b907-1e30a769f107',1745985600,'Wireframe/basic lineups user interface',3.5,25.0,87.5,1752274548,11);
INSERT INTO beenvoice_invoice_item VALUES('cf1c9e48-bf50-4083-b482-9338a3c439d0','0c9a6715-70f8-4f83-ab01-a8340773431d',1749096000,'Enhance PointsUpload page',3.5,25.0,87.5,1752278188,0);
INSERT INTO beenvoice_invoice_item VALUES('212d7b08-2d12-449a-a0f9-c4496819b740','0c9a6715-70f8-4f83-ab01-a8340773431d',1749441600,'Handle ties in points section',3.5,25.0,87.5,1752278188,1);
INSERT INTO beenvoice_invoice_item VALUES('0d6d372f-6679-4dea-b78b-03ef0192c1e4','0c9a6715-70f8-4f83-ab01-a8340773431d',1749528000,'Add manipulation of bulk email contact lists',4.0,25.0,100.0,1752278188,2);
INSERT INTO beenvoice_invoice_item VALUES('58dfc4ef-8498-4630-a62f-b5fd20410e6e','0c9a6715-70f8-4f83-ab01-a8340773431d',1749614400,'Add staff list to email system, create new',3.5,25.0,87.5,1752278188,3);
INSERT INTO beenvoice_invoice_item VALUES('513c952b-c0f7-49ee-948d-41e5ca4d6e83','0c9a6715-70f8-4f83-ab01-a8340773431d',1749700800,'Add rain banner functionality to events',4.0,25.0,100.0,1752278188,4);
INSERT INTO beenvoice_invoice_item VALUES('469256a8-8335-48ce-a001-67928accf01c','0c9a6715-70f8-4f83-ab01-a8340773431d',1750046400,'Social Media code of conduct',2.0,25.0,50.0,1752278188,5);
INSERT INTO beenvoice_invoice_item VALUES('30720638-2128-4017-897a-8d635d541246','0c9a6715-70f8-4f83-ab01-a8340773431d',1750219200,'Active status management, event cleanup',3.75,25.0,93.75,1752278188,6);
INSERT INTO beenvoice_invoice_item VALUES('d403fc8d-72d3-4d75-a91e-9b3cf68df820','0c9a6715-70f8-4f83-ab01-a8340773431d',1750305600,'Google/Apple Calendar Sync from events',4.5,25.0,112.5,1752278188,7);
INSERT INTO beenvoice_invoice_item VALUES('217f013d-861a-406e-bd8e-392659f6ba72','0c9a6715-70f8-4f83-ab01-a8340773431d',1750392000,'In person, printers/email/server updates',5.0,25.0,125.0,1752278188,8);
INSERT INTO beenvoice_invoice_item VALUES('52be1c1f-3523-4bc3-a8ab-66902db5e229','0c9a6715-70f8-4f83-ab01-a8340773431d',1750478400,'Race day, Server/Handicapping',6.0,25.0,150.0,1752278188,9);
INSERT INTO beenvoice_invoice_item VALUES('dee51491-b6b1-4038-a641-d4fcdfe42f95','0c9a6715-70f8-4f83-ab01-a8340773431d',1750651200,'Repair sponsors/Plan out permissions',3.5,25.0,87.5,1752278188,10);
INSERT INTO beenvoice_invoice_item VALUES('0bd1bec4-2541-42db-ae38-d86d9bac43d5','0c9a6715-70f8-4f83-ab01-a8340773431d',1750737600,'Backend permissions implementation',5.5,25.0,137.5,1752278188,11);
INSERT INTO beenvoice_invoice_item VALUES('dbcb12d5-9b37-4f65-9275-56d82338601b','0c9a6715-70f8-4f83-ab01-a8340773431d',1750824000,'Frontend permissions/deployment',5.0,25.0,125.0,1752278188,12);
INSERT INTO beenvoice_invoice_item VALUES('2899f8ae-6f76-4f32-8350-09151b3d76ab','0c9a6715-70f8-4f83-ab01-a8340773431d',1750910400,'Plan out and begin migration to races sys',4.5,25.0,112.5,1752278188,13);
INSERT INTO beenvoice_invoice_item VALUES('f6f46a67-83ac-4bb1-b128-82daf0063128','0c9a6715-70f8-4f83-ab01-a8340773431d',1750996800,'Replace eventDivisions with races',5.0,25.0,125.0,1752278188,14);
INSERT INTO beenvoice_invoice_item VALUES('56e676ae-3de1-4039-b3d6-e5da99c5aa0c','0c9a6715-70f8-4f83-ab01-a8340773431d',1751083200,'In person, race day, media, development',8.0,25.0,200.0,1752278188,15);
INSERT INTO beenvoice_invoice_item VALUES('71fb8bc8-ac75-426b-a624-83bbaebbac1c','0c9a6715-70f8-4f83-ab01-a8340773431d',1751169600,'User interface for race editing',5.5,25.0,137.5,1752278188,16);
INSERT INTO beenvoice_invoice_item VALUES('eb64faf3-2a9b-4f66-8dd9-4f39f6a7af05','0c9a6715-70f8-4f83-ab01-a8340773431d',1751256000,'Public user interface for finishes and lineup',5.5,25.0,137.5,1752278188,17);
INSERT INTO beenvoice_invoice_item VALUES('79b80323-6c8a-4562-a274-f9e697b1efe4','0c9a6715-70f8-4f83-ab01-a8340773431d',1751342400,'Production push pt.1',6.0,25.0,150.0,1752278188,18);
INSERT INTO beenvoice_invoice_item VALUES('cd84469d-f608-4edd-9121-4366041fe25a','0c9a6715-70f8-4f83-ab01-a8340773431d',1751428800,'Production database migration',3.0,25.0,75.0,1752278188,19);
INSERT INTO beenvoice_invoice_item VALUES('93d21511-d3f9-4338-8eb4-3233614c4ae0','0c9a6715-70f8-4f83-ab01-a8340773431d',1751774400,'Testing, data entry from old races begin',4.0,25.0,100.0,1752278188,20);
INSERT INTO beenvoice_invoice_item VALUES('e701eb75-8ce0-4194-812a-2a3520487a00','0c9a6715-70f8-4f83-ab01-a8340773431d',1751860800,'Update pricing queries, 2023 races',4.0,25.0,100.0,1752278188,21);

View File

@@ -8,7 +8,7 @@ import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { toast } from "sonner";
import { Logo } from "~/components/logo";
import { Logo } from "~/components/branding/logo";
import { User, Mail, Lock, ArrowRight } from "lucide-react";
function RegisterForm() {

View File

@@ -9,7 +9,7 @@ import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { toast } from "sonner";
import { Logo } from "~/components/logo";
import { Logo } from "~/components/branding/logo";
import { Mail, Lock, ArrowRight } from "lucide-react";
function SignInForm() {

View File

@@ -1,7 +1,7 @@
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientForm } from "~/components/client-form";
import { ClientForm } from "~/components/forms/client-form";
import Link from "next/link";
interface EditClientPageProps {

View File

@@ -1,5 +1,5 @@
import { Navbar } from "~/components/Navbar";
import { Sidebar } from "~/components/Sidebar";
import { Navbar } from "~/components/layout/navbar";
import { Sidebar } from "~/components/layout/sidebar";
export default function ClientsLayout({
children,
@@ -17,4 +17,4 @@ export default function ClientsLayout({
</div>
</>
);
}
}

View File

@@ -1,7 +1,7 @@
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientForm } from "~/components/client-form";
import { ClientForm } from "~/components/forms/client-form";
import Link from "next/link";
export default async function NewClientPage() {
@@ -34,4 +34,4 @@ export default async function NewClientPage() {
</div>
</HydrateClient>
);
}
}

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientList } from "~/components/client-list";
import { ClientList } from "~/components/data/client-list";
import { Plus } from "lucide-react";
export default async function ClientsPage() {
@@ -39,4 +39,4 @@ export default async function ClientsPage() {
</div>
</HydrateClient>
);
}
}

View File

@@ -1,263 +0,0 @@
"use client";
import { api } from "~/trpc/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { StatusBadge, type StatusType } from "~/components/ui/status-badge";
import {
Users,
FileText,
TrendingUp,
Calendar,
Plus,
ArrowRight,
} from "lucide-react";
import Link from "next/link";
import {
DashboardStatsSkeleton,
DashboardActivitySkeleton,
} from "~/components/ui/skeleton";
// Client component for dashboard stats
export function DashboardStats() {
const { data: clients, isLoading: clientsLoading } =
api.clients.getAll.useQuery();
const { data: invoices, isLoading: invoicesLoading } =
api.invoices.getAll.useQuery();
if (clientsLoading || invoicesLoading) {
return <DashboardStatsSkeleton />;
}
const totalClients = clients?.length ?? 0;
const totalInvoices = invoices?.length ?? 0;
const totalRevenue =
invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) ?? 0;
const pendingInvoices =
invoices?.filter(
(invoice) => invoice.status === "sent" || invoice.status === "draft",
).length ?? 0;
// Calculate month-over-month changes (simplified)
const lastMonthClients = 0; // This would need historical data
const lastMonthInvoices = 0;
const lastMonthRevenue = 0;
return (
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-muted-foreground text-sm font-medium">
Total Clients
</CardTitle>
<div className="rounded-lg bg-emerald-100 p-2">
<Users className="h-4 w-4 text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-emerald-600">
{totalClients}
</div>
<p className="text-muted-foreground text-xs">
{totalClients > lastMonthClients ? "+" : ""}
{totalClients - lastMonthClients} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-muted-foreground text-sm font-medium">
Total Invoices
</CardTitle>
<div className="rounded-lg bg-blue-100 p-2">
<FileText className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">
{totalInvoices}
</div>
<p className="text-muted-foreground text-xs">
{totalInvoices > lastMonthInvoices ? "+" : ""}
{totalInvoices - lastMonthInvoices} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-muted-foreground text-sm font-medium">
Revenue
</CardTitle>
<div className="rounded-lg bg-teal-100 p-2">
<TrendingUp className="h-4 w-4 text-teal-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-teal-600">
${totalRevenue.toFixed(2)}
</div>
<p className="text-muted-foreground text-xs">
{totalRevenue > lastMonthRevenue ? "+" : ""}
{(
((totalRevenue - lastMonthRevenue) / (lastMonthRevenue || 1)) *
100
).toFixed(1)}
% from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-muted-foreground text-sm font-medium">
Pending Invoices
</CardTitle>
<div className="rounded-lg bg-orange-100 p-2">
<Calendar className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">
{pendingInvoices}
</div>
<p className="text-muted-foreground text-xs">Due this month</p>
</CardContent>
</Card>
</div>
);
}
// Client component for dashboard cards
export function DashboardCards() {
return (
<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="rounded-lg bg-emerald-100 p-2">
<Users className="h-5 w-5" />
</div>
Manage Clients
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">
Add new clients and manage your existing client relationships.
</p>
<div className="flex gap-3">
<Button asChild variant="brand">
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-4 w-4" />
Add Client
</Link>
</Button>
<Button variant="outline" asChild className="font-medium">
<Link href="/dashboard/clients">
View All Clients
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
<Card className="shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="rounded-lg bg-emerald-100 p-2">
<FileText className="h-5 w-5" />
</div>
Create Invoices
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">
Generate professional invoices and track payments.
</p>
<div className="flex gap-3">
<Button asChild variant="brand">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
New Invoice
</Link>
</Button>
<Button variant="outline" asChild className="font-medium">
<Link href="/dashboard/invoices">
View All Invoices
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
// Client component for recent activity
export function DashboardActivity() {
const { data: invoices, isLoading } = api.invoices.getAll.useQuery();
if (isLoading) {
return <DashboardActivitySkeleton />;
}
const recentInvoices = invoices?.slice(0, 5) ?? [];
return (
<Card className="shadow-xl backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-emerald-700">Recent Activity</CardTitle>
</CardHeader>
<CardContent>
{recentInvoices.length === 0 ? (
<div className="text-muted-foreground py-12 text-center">
<div className="bg-muted mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full p-4">
<FileText className="text-muted-foreground h-8 w-8" />
</div>
<p className="text-foreground mb-2 text-lg font-medium">
No recent activity
</p>
<p className="text-muted-foreground text-sm">
Start by adding your first client or creating an invoice
</p>
</div>
) : (
<div className="space-y-4">
{recentInvoices.map((invoice) => (
<div
key={invoice.id}
className="bg-muted/50 flex items-center justify-between rounded-lg p-4"
>
<div className="flex items-center gap-3">
<div className="rounded-lg bg-emerald-100 p-2">
<FileText className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="text-foreground font-medium">
Invoice #{invoice.invoiceNumber}
</p>
<p className="text-muted-foreground text-sm">
{invoice.client?.name ?? "Unknown Client"} $
{invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<StatusBadge status={invoice.status as StatusType} />
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -2,8 +2,8 @@
import Link from "next/link";
import { useParams } from "next/navigation";
import { BusinessForm } from "~/components/business-form";
import { PageHeader } from "~/components/page-header";
import { BusinessForm } from "~/components/forms/business-form";
import { PageHeader } from "~/components/layout/page-header";
export default function EditBusinessPage() {
const params = useParams();

View File

@@ -3,7 +3,7 @@ import { api } from "~/trpc/server";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import Link from "next/link";
import {
Edit,

View File

@@ -3,7 +3,7 @@
import Link from "next/link";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "~/components/ui/button";
import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table";
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table";
import { Building, Pencil, Trash2, ExternalLink } from "lucide-react";
import { useState } from "react";
import {

View File

@@ -1,7 +1,7 @@
"use client";
import { api } from "~/trpc/react";
import { DataTableSkeleton } from "~/components/ui/data-table";
import { DataTableSkeleton } from "~/components/data/data-table";
import { BusinessesDataTable } from "./businesses-data-table";
export function BusinessesTable() {

View File

@@ -1,6 +1,6 @@
import Link from "next/link";
import { BusinessForm } from "~/components/business-form";
import { PageHeader } from "~/components/page-header";
import { BusinessForm } from "~/components/forms/business-form";
import { PageHeader } from "~/components/layout/page-header";
import { HydrateClient } from "~/trpc/server";
export default function NewBusinessPage() {

View File

@@ -3,8 +3,8 @@ import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { Plus } from "lucide-react";
import { BusinessesTable } from "./_components/businesses-table";
import { PageHeader } from "~/components/page-header";
import { PageContent, PageSection } from "~/components/ui/page-layout";
import { PageHeader } from "~/components/layout/page-header";
import { PageContent, PageSection } from "~/components/layout/page-layout";
export default async function BusinessesPage() {
return (

View File

@@ -1,7 +1,7 @@
import Link from "next/link";
import { HydrateClient } from "~/trpc/server";
import { ClientForm } from "~/components/client-form";
import { PageHeader } from "~/components/page-header";
import { ClientForm } from "~/components/forms/client-form";
import { PageHeader } from "~/components/layout/page-header";
interface EditClientPageProps {
params: Promise<{ id: string }>;

View File

@@ -3,7 +3,7 @@ import { api } from "~/trpc/server";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import Link from "next/link";
import {
Edit,

View File

@@ -3,7 +3,7 @@
import Link from "next/link";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "~/components/ui/button";
import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table";
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table";
import { UserPlus, Pencil, Trash2 } from "lucide-react";
import { useState } from "react";
import {

View File

@@ -1,7 +1,7 @@
"use client";
import { api } from "~/trpc/react";
import { DataTableSkeleton } from "~/components/ui/data-table";
import { DataTableSkeleton } from "~/components/data/data-table";
import { ClientsDataTable } from "./clients-data-table";
export function ClientsTable() {

View File

@@ -1,7 +1,7 @@
import Link from "next/link";
import { HydrateClient } from "~/trpc/server";
import { ClientForm } from "~/components/client-form";
import { PageHeader } from "~/components/page-header";
import { ClientForm } from "~/components/forms/client-form";
import { PageHeader } from "~/components/layout/page-header";
export default async function NewClientPage() {
return (

View File

@@ -3,8 +3,8 @@ import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { Plus } from "lucide-react";
import { ClientsTable } from "./_components/clients-table";
import { PageHeader } from "~/components/page-header";
import { PageContent, PageSection } from "~/components/ui/page-layout";
import { PageHeader } from "~/components/layout/page-header";
import { PageContent, PageSection } from "~/components/layout/page-layout";
export default async function ClientsPage() {
return (

View File

@@ -0,0 +1,64 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Button } from "~/components/ui/button";
import {
MoreHorizontal,
Edit,
Copy,
Send,
Trash2,
} from "lucide-react";
interface InvoiceActionsDropdownProps {
invoiceId: string;
}
export function InvoiceActionsDropdown({ invoiceId }: InvoiceActionsDropdownProps) {
const handleSendClick = () => {
const sendButton = document.querySelector(
"[data-testid='send-invoice-button']",
) as HTMLButtonElement;
sendButton?.click();
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="border-0 shadow-sm"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem>
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</DropdownMenuItem>
<DropdownMenuItem>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSendClick}>
<Send className="mr-2 h-4 w-4" />
Send to Client
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,186 @@
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import { Separator } from "~/components/ui/separator";
import { Skeleton } from "~/components/ui/skeleton";
export function InvoiceDetailsSkeleton() {
return (
<div className="space-y-6">
<div className="grid gap-6 xl:grid-cols-3">
{/* Main Content */}
<div className="space-y-6 xl:col-span-2">
{/* Invoice Header Skeleton */}
<Card className="border-0 shadow-sm">
<CardContent className="p-4 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<Skeleton className="h-6 w-48 sm:h-8" />
<Skeleton className="h-6 w-16" />
</div>
<Skeleton className="mt-1 h-4 w-64" />
</div>
<div className="text-left sm:text-right">
<Skeleton className="h-4 w-20" />
<Skeleton className="mt-1 h-6 w-24 sm:h-8" />
</div>
</div>
</CardContent>
</Card>
{/* Client & Business Information Skeleton */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{Array.from({ length: 2 }).map((_, i) => (
<Card key={i} className="border-0 shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 sm:h-5 sm:w-5" />
<Skeleton className="h-6 w-16" />
</div>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<Skeleton className="h-5 w-32 sm:h-6" />
<div className="space-y-2 sm:space-y-3">
{Array.from({ length: 3 }).map((_, j) => (
<div key={j} className="flex items-center gap-2 sm:gap-3">
<Skeleton className="h-6 w-6 rounded-lg sm:h-8 sm:w-8" />
<Skeleton className="h-3 w-28 sm:h-4" />
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
{/* Invoice Items Skeleton */}
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 sm:h-5 sm:w-5" />
<Skeleton className="h-5 w-28 sm:h-6" />
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[500px]">
<thead>
<tr className="border-b">
{["Date", "Description", "Hours", "Rate", "Amount"].map(
(header) => (
<th key={header} className="p-2 text-left sm:p-4">
<Skeleton className="h-3 w-16 sm:h-4" />
</th>
),
)}
</tr>
</thead>
<tbody>
{Array.from({ length: 3 }).map((_, i) => (
<tr key={i} className="border-b last:border-0">
<td className="p-2 sm:p-4">
<Skeleton className="h-3 w-20 sm:h-4" />
</td>
<td className="p-2 sm:p-4">
<Skeleton className="h-3 w-48 sm:h-4" />
</td>
<td className="p-2 sm:p-4">
<Skeleton className="h-3 w-12 sm:h-4" />
</td>
<td className="p-2 sm:p-4">
<Skeleton className="h-3 w-16 sm:h-4" />
</td>
<td className="p-2 sm:p-4">
<Skeleton className="h-3 w-20 sm:h-4" />
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Totals Section Skeleton */}
<div className="bg-muted/20 border-t p-3 sm:p-4">
<div className="flex justify-end">
<div className="w-full max-w-64 space-y-2">
<div className="flex justify-between">
<Skeleton className="h-3 w-16 sm:h-4" />
<Skeleton className="h-3 w-20 sm:h-4" />
</div>
<div className="flex justify-between">
<Skeleton className="h-3 w-20 sm:h-4" />
<Skeleton className="h-3 w-20 sm:h-4" />
</div>
<Separator />
<div className="flex justify-between">
<Skeleton className="h-4 w-12 sm:h-6" />
<Skeleton className="h-4 w-24 sm:h-6" />
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Notes Skeleton */}
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<Skeleton className="h-5 w-16 sm:h-6" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-3 w-full sm:h-4" />
<Skeleton className="h-3 w-3/4 sm:h-4" />
<Skeleton className="h-3 w-1/2 sm:h-4" />
</div>
</CardContent>
</Card>
</div>
{/* Sidebar Skeleton */}
<div className="space-y-4 sm:space-y-6">
{/* Actions Skeleton */}
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<Skeleton className="h-5 w-16 sm:h-6" />
</CardHeader>
<CardContent className="space-y-2 sm:space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full sm:h-10" />
))}
</CardContent>
</Card>
{/* Details Skeleton */}
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 sm:h-5 sm:w-5" />
<Skeleton className="h-5 w-16 sm:h-6" />
</div>
</CardHeader>
<CardContent className="space-y-2 sm:space-y-3">
<div className="grid grid-cols-2 gap-2 sm:gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-1">
<Skeleton className="h-3 w-16 sm:h-4" />
<Skeleton className="h-3 w-20 sm:h-4" />
</div>
))}
</div>
</CardContent>
</Card>
{/* Danger Zone Skeleton */}
<Card className="border-red-200 shadow-sm dark:border-red-800">
<CardHeader className="pb-3">
<Skeleton className="h-5 w-24 sm:h-6" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-full sm:h-10" />
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
"use client";
import type { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "~/components/data/data-table";
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(date));
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
// Type for invoice item data
interface InvoiceItem {
id: string;
invoiceId: string;
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
position: number;
createdAt: Date;
}
interface InvoiceItemsTableProps {
items: InvoiceItem[];
}
const columns: ColumnDef<InvoiceItem>[] = [
{
accessorKey: "date",
header: "Date",
cell: ({ row }) => formatDate(row.getValue("date")),
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<div className="font-medium">{row.getValue("description")}</div>
),
},
{
accessorKey: "hours",
header: "Hours",
cell: ({ row }) => (
<div className="text-right">{row.getValue("hours")}</div>
),
},
{
accessorKey: "rate",
header: "Rate",
cell: ({ row }) => (
<div className="text-right">{formatCurrency(row.getValue("rate"))}</div>
),
},
{
accessorKey: "amount",
header: "Amount",
cell: ({ row }) => (
<div className="text-right font-medium text-emerald-600">
{formatCurrency(row.getValue("amount"))}
</div>
),
},
];
export function InvoiceItemsTable({ items }: InvoiceItemsTableProps) {
return (
<DataTable
columns={columns}
data={items}
showSearch={false}
showColumnVisibility={false}
showPagination={false}
/>
);
}

View File

@@ -3,84 +3,43 @@
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { toast } from "sonner";
import { api } from "~/trpc/react";
import { generateInvoicePDF } from "~/lib/pdf-export";
import { Download, Loader2 } from "lucide-react";
interface Invoice {
id: string;
invoiceNumber: string;
issueDate: Date;
dueDate: Date;
status: string;
totalAmount: number;
taxRate: number;
notes?: string | null;
business?: {
name: string;
email?: string | null;
phone?: string | null;
addressLine1?: string | null;
addressLine2?: string | null;
city?: string | null;
state?: string | null;
postalCode?: string | null;
country?: string | null;
website?: string | null;
taxId?: string | null;
} | null;
client: {
name: string;
email?: string | null;
phone?: string | null;
addressLine1?: string | null;
addressLine2?: string | null;
city?: string | null;
state?: string | null;
postalCode?: string | null;
country?: string | null;
};
items: Array<{
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
}>;
}
interface PDFDownloadButtonProps {
invoice: Invoice;
variant?: "button" | "menu" | "icon";
invoiceId: string;
variant?: "default" | "outline" | "ghost" | "icon";
className?: string;
}
export function PDFDownloadButton({
invoice,
variant = "button",
invoiceId,
variant = "outline",
className,
}: PDFDownloadButtonProps) {
const [isGenerating, setIsGenerating] = useState(false);
// Fetch invoice data when PDF generation is triggered
const { refetch: fetchInvoice } = api.invoices.getById.useQuery(
{ id: invoiceId },
{ enabled: false },
);
const handleDownloadPDF = async () => {
if (isGenerating) return;
setIsGenerating(true);
try {
// Transform the invoice data to match the PDF interface
const pdfData = {
invoiceNumber: invoice.invoiceNumber,
issueDate: invoice.issueDate,
dueDate: invoice.dueDate,
status: invoice.status,
totalAmount: invoice.totalAmount,
taxRate: invoice.taxRate,
notes: invoice.notes,
business: invoice.business,
client: invoice.client,
items: invoice.items,
};
// Fetch fresh invoice data
const { data: invoiceData } = await fetchInvoice();
await generateInvoicePDF(pdfData);
if (!invoiceData) {
throw new Error("Invoice not found");
}
await generateInvoicePDF(invoiceData);
toast.success("PDF downloaded successfully");
} catch (error) {
console.error("PDF generation error:", error);
@@ -92,23 +51,6 @@ export function PDFDownloadButton({
}
};
if (variant === "menu") {
return (
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="hover:bg-accent flex w-full items-center gap-2 px-2 py-1.5 text-sm"
>
{isGenerating ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
{isGenerating ? "Generating..." : "Download PDF"}
</button>
);
}
if (variant === "icon") {
return (
<Button
@@ -116,12 +58,12 @@ export function PDFDownloadButton({
disabled={isGenerating}
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
className={className}
>
{isGenerating ? (
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="h-3 w-3 animate-spin sm:h-4 sm:w-4" />
) : (
<Download className="h-4 w-4" />
<Download className="h-3 w-3 sm:h-4 sm:w-4" />
)}
</Button>
);
@@ -131,15 +73,21 @@ export function PDFDownloadButton({
<Button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="w-full justify-start"
variant="outline"
variant={variant}
size="default"
className={`w-full shadow-sm ${className}`}
>
{isGenerating ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Generating PDF...</span>
</>
) : (
<Download className="mr-2 h-4 w-4" />
<>
<Download className="mr-2 h-4 w-4" />
<span>Download PDF</span>
</>
)}
{isGenerating ? "Generating..." : "Download PDF"}
</Button>
);
}

View File

@@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { toast } from "sonner";
import { api } from "~/trpc/react";
import { generateInvoicePDFBlob } from "~/lib/pdf-export";
import { Send, Loader2 } from "lucide-react";
interface SendInvoiceButtonProps {
invoiceId: string;
variant?: "default" | "outline" | "ghost" | "icon";
className?: string;
}
export function SendInvoiceButton({
invoiceId,
variant = "outline",
className,
}: SendInvoiceButtonProps) {
const [isSending, setIsSending] = useState(false);
// Fetch invoice data when sending is triggered
const { refetch: fetchInvoice } = api.invoices.getById.useQuery(
{ id: invoiceId },
{ enabled: false },
);
const handleSendInvoice = async () => {
if (isSending) return;
setIsSending(true);
try {
// Fetch fresh invoice data
const { data: invoice } = await fetchInvoice();
if (!invoice) {
throw new Error("Invoice not found");
}
// Generate PDF blob for potential attachment
const pdfBlob = await generateInvoicePDFBlob(invoice);
// Create a temporary download URL for the PDF
const pdfUrl = URL.createObjectURL(pdfBlob);
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
// Format date
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
};
// Calculate days until due
const today = new Date();
const dueDate = new Date(invoice.dueDate);
const daysUntilDue = Math.ceil(
(dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
// Create professional email template
const subject = `Invoice ${invoice.invoiceNumber} - ${formatCurrency(invoice.totalAmount)}`;
const body = `Dear ${invoice.client.name},
I hope this email finds you well. Please find attached invoice ${invoice.invoiceNumber} for the services provided.
Invoice Details:
• Invoice Number: ${invoice.invoiceNumber}
• Issue Date: ${formatDate(invoice.issueDate)}
• Due Date: ${formatDate(invoice.dueDate)}
• Amount Due: ${formatCurrency(invoice.totalAmount)}
${daysUntilDue > 0 ? `• Payment Due: In ${daysUntilDue} days` : daysUntilDue === 0 ? `• Payment Due: Today` : `• Status: ${Math.abs(daysUntilDue)} days overdue`}
${invoice.notes ? `\nAdditional Notes:\n${invoice.notes}\n` : ""}
Please review the attached invoice and remit payment by the due date. If you have any questions or concerns regarding this invoice, please don't hesitate to contact me.
Thank you for your business!
Best regards,
${invoice.business?.name ?? "Your Business Name"}
${invoice.business?.email ? `\n${invoice.business.email}` : ""}
${invoice.business?.phone ? `\n${invoice.business.phone}` : ""}`;
// Create mailto link
const mailtoLink = `mailto:${invoice.client.email ?? ""}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
// Create a temporary link element to trigger mailto
const link = document.createElement("a");
link.href = mailtoLink;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the PDF URL object
URL.revokeObjectURL(pdfUrl);
toast.success("Email client opened with invoice details");
} catch (error) {
console.error("Send invoice error:", error);
toast.error(
error instanceof Error
? error.message
: "Failed to prepare invoice email",
);
} finally {
setIsSending(false);
}
};
if (variant === "icon") {
return (
<Button
onClick={handleSendInvoice}
disabled={isSending}
variant="ghost"
size="sm"
className={className}
>
{isSending ? (
<Loader2 className="h-3 w-3 animate-spin sm:h-4 sm:w-4" />
) : (
<Send className="h-3 w-3 sm:h-4 sm:w-4" />
)}
</Button>
);
}
return (
<Button
onClick={handleSendInvoice}
disabled={isSending}
variant={variant}
size="default"
className={`w-full shadow-sm ${className}`}
data-testid="send-invoice-button"
>
{isSending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Preparing Email...</span>
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
<span>Send Invoice</span>
</>
)}
</Button>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { InvoiceView } from "~/components/invoice-view";
import { InvoiceForm } from "~/components/invoice-form";
import { InvoiceView } from "~/components/data/invoice-view";
import { InvoiceForm } from "~/components/forms/invoice-form";
interface UnifiedInvoicePageProps {
invoiceId: string;

View File

@@ -1,56 +1,53 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { useRouter, useParams } from "next/navigation";
import {
ArrowLeft,
Building,
DollarSign,
Edit3,
Eye,
FileText,
Hash,
Loader2,
Plus,
Save,
Send,
Trash2,
User,
} from "lucide-react";
import Link from "next/link";
import { api } from "~/trpc/react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { NumberInput } from "~/components/ui/number-input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { PageHeader } from "~/components/page-header";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { PageHeader } from "~/components/layout/page-header";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { DatePicker } from "~/components/ui/date-picker";
import { FloatingActionBar } from "~/components/ui/floating-action-bar";
import { toast } from "sonner";
import { Label } from "~/components/ui/label";
import { NumberInput } from "~/components/ui/number-input";
import {
ArrowLeft,
Save,
Plus,
Trash2,
FileText,
Building,
User,
Loader2,
Send,
DollarSign,
Hash,
Edit3,
Eye,
} from "lucide-react";
interface EditInvoicePageProps {}
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Separator } from "~/components/ui/separator";
import { Textarea } from "~/components/ui/textarea";
import { api } from "~/trpc/react";
interface InvoiceItem {
id?: string;

View File

@@ -4,78 +4,34 @@ import Link from "next/link";
import { api, HydrateClient } from "~/trpc/server";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { StatusBadge, type StatusType } from "~/components/ui/status-badge";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { Separator } from "~/components/ui/separator";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import { PDFDownloadButton } from "./_components/pdf-download-button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { SendInvoiceButton } from "./_components/send-invoice-button";
import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton";
import { InvoiceActionsDropdown } from "./_components/invoice-actions-dropdown";
import { InvoiceItemsTable } from "./_components/invoice-items-table";
import {
ArrowLeft,
Edit,
Send,
Copy,
MoreHorizontal,
CheckCircle,
Clock,
Calendar,
FileText,
Building,
User,
DollarSign,
Hash,
MapPin,
Calendar,
Copy,
Edit,
FileText,
Mail,
MapPin,
Phone,
User,
AlertTriangle,
Trash2,
} from "lucide-react";
interface InvoicePageProps {
params: Promise<{ id: string }>;
}
function InvoiceStatusBadge({
status,
dueDate,
}: {
status: string;
dueDate: Date;
}) {
const getStatus = (): "draft" | "sent" | "paid" | "overdue" => {
if (status === "paid") return "paid";
if (status === "draft") return "draft";
if (status === "sent") {
const due = new Date(dueDate);
return due < new Date() ? "overdue" : "sent";
}
return "draft";
};
const actualStatus = getStatus();
const icons = {
draft: FileText,
sent: Clock,
paid: CheckCircle,
overdue: Clock,
};
const Icon = icons[actualStatus];
return (
<StatusBadge status={actualStatus} className="flex items-center gap-1">
<Icon className="h-3 w-3" />
{actualStatus.charAt(0).toUpperCase() + actualStatus.slice(1)}
</StatusBadge>
);
}
async function InvoiceDetails({ invoiceId }: { invoiceId: string }) {
async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
const invoice = await api.invoices.getById({ id: invoiceId });
if (!invoice) {
@@ -97,379 +53,337 @@ async function InvoiceDetails({ invoiceId }: { invoiceId: string }) {
}).format(amount);
};
const subtotal =
invoice.items?.reduce((sum, item) => sum + item.hours * item.rate, 0) || 0;
const taxAmount = (subtotal * (invoice.taxRate || 0)) / 100;
const total = subtotal + taxAmount;
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
const taxAmount = (subtotal * invoice.taxRate) / 100;
const isOverdue =
new Date(invoice.dueDate) < new Date() && invoice.status !== "paid";
const getStatusType = (): StatusType => {
if (invoice.status === "paid") return "paid";
if (invoice.status === "draft") return "draft";
if (invoice.status === "sent") {
return isOverdue ? "overdue" : "sent";
}
return "draft";
};
return (
<div className="space-y-6">
{/* Invoice Header */}
<Card className="border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
{/* Invoice Info */}
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="rounded-lg bg-emerald-100 p-3 dark:bg-emerald-900/30">
<Hash className="h-6 w-6 text-emerald-600" />
</div>
<div>
<h1 className="text-2xl font-bold text-emerald-700 dark:text-emerald-400">
{invoice.invoiceNumber}
</h1>
<p className="text-muted-foreground text-sm">Invoice</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div className="flex items-center gap-2">
<Calendar className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-muted-foreground text-xs font-medium">
Issued
</p>
<p className="text-sm font-semibold">
{formatDate(invoice.issueDate)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-muted-foreground text-xs font-medium">
Due
</p>
<p className="text-sm font-semibold">
{formatDate(invoice.dueDate)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<DollarSign className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-muted-foreground text-xs font-medium">
Amount
</p>
<p className="text-sm font-semibold text-emerald-600">
{formatCurrency(total)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<FileText className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-muted-foreground text-xs font-medium">
Status
</p>
<InvoiceStatusBadge
status={invoice.status}
dueDate={invoice.dueDate}
/>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex flex-col gap-2 sm:flex-row lg:flex-col">
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button className="w-full">
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</Button>
</Link>
<PDFDownloadButton invoice={invoice} variant="button" />
</div>
</div>
</CardContent>
</Card>
{/* Business & Client Info */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* From Business */}
<Card className="border-0 shadow-md">
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-lg">
<Building className="h-4 w-4 text-emerald-600" />
From
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{invoice.business ? (
<>
<div>
<p className="font-semibold">{invoice.business.name}</p>
</div>
<div className="space-y-1">
{invoice.business.email && (
<div className="flex items-center gap-2 text-sm">
<Mail className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">
{invoice.business.email}
</span>
</div>
)}
{invoice.business.phone && (
<div className="flex items-center gap-2 text-sm">
<Phone className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">
{invoice.business.phone}
</span>
</div>
)}
{invoice.business.addressLine1 && (
<div className="flex items-start gap-2 text-sm">
<MapPin className="text-muted-foreground mt-0.5 h-3 w-3" />
<div className="text-muted-foreground">
<p>{invoice.business.addressLine1}</p>
{invoice.business.addressLine2 && (
<p>{invoice.business.addressLine2}</p>
)}
<p>
{[
invoice.business.city,
invoice.business.state,
invoice.business.postalCode,
]
.filter(Boolean)
.join(", ")}
</p>
{invoice.business.country && (
<p>{invoice.business.country}</p>
)}
</div>
</div>
)}
</div>
</>
) : (
<p className="text-muted-foreground text-sm italic">
No business information
</p>
)}
</CardContent>
</Card>
{/* To Client */}
<Card className="border-0 shadow-md">
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-lg">
<User className="h-4 w-4 text-emerald-600" />
Bill To
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<p className="font-semibold">{invoice.client.name}</p>
</div>
<div className="space-y-1">
{invoice.client.email && (
<div className="flex items-center gap-2 text-sm">
<Mail className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">
{invoice.client.email}
</span>
</div>
)}
{invoice.client.phone && (
<div className="flex items-center gap-2 text-sm">
<Phone className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">
{invoice.client.phone}
</span>
</div>
)}
{invoice.client.addressLine1 && (
<div className="flex items-start gap-2 text-sm">
<MapPin className="text-muted-foreground mt-0.5 h-3 w-3" />
<div className="text-muted-foreground">
<p>{invoice.client.addressLine1}</p>
{invoice.client.addressLine2 && (
<p>{invoice.client.addressLine2}</p>
)}
<p>
{[
invoice.client.city,
invoice.client.state,
invoice.client.postalCode,
]
.filter(Boolean)
.join(", ")}
</p>
{invoice.client.country && <p>{invoice.client.country}</p>}
</div>
</div>
)}
{/* Overdue Alert */}
{isOverdue && (
<Card className="border-red-200 bg-red-50">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-red-700">
<AlertTriangle className="h-5 w-5" />
<span className="font-medium">
This invoice is{" "}
{Math.ceil(
(new Date().getTime() - new Date(invoice.dueDate).getTime()) /
(1000 * 60 * 60 * 24),
)}{" "}
days overdue
</span>
</div>
</CardContent>
</Card>
</div>
)}
{/* Line Items */}
<Card className="border-0 shadow-lg">
<CardHeader className="border-b">
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5 text-emerald-600" />
Line Items ({invoice.items?.length || 0})
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
{invoice.items && invoice.items.length > 0 ? (
<div className="space-y-0">
{/* Header - Hidden on mobile */}
<div className="border-muted/30 bg-muted/20 hidden grid-cols-12 gap-4 border-b px-6 py-3 text-sm font-medium md:grid">
<div className="col-span-2">Date</div>
<div className="col-span-5">Description</div>
<div className="col-span-2 text-right">Hours</div>
<div className="col-span-2 text-right">Rate</div>
<div className="col-span-1 text-right">Amount</div>
</div>
{/* Items */}
{invoice.items.map((item, index) => (
<div
key={index}
className="border-muted/30 grid grid-cols-1 gap-2 border-b px-6 py-4 last:border-b-0 md:grid-cols-12 md:items-center md:gap-4"
>
{/* Mobile Layout */}
<div className="md:hidden">
<div className="mb-2 flex items-start justify-between">
<p className="font-medium">{item.description}</p>
<span className="font-mono text-sm font-semibold text-emerald-600">
{formatCurrency(item.hours * item.rate)}
</span>
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div>
<span className="text-muted-foreground text-xs">
Date
</span>
<p>{formatDate(item.date)}</p>
</div>
<div>
<span className="text-muted-foreground text-xs">
Hours
</span>
<p className="font-mono">{item.hours}</p>
</div>
<div>
<span className="text-muted-foreground text-xs">
Rate
</span>
<p className="font-mono">
{formatCurrency(item.rate)}
</p>
</div>
</div>
</div>
{/* Desktop Layout */}
<div className="text-muted-foreground col-span-2 hidden text-sm md:block">
{formatDate(item.date)}
</div>
<div className="col-span-5 hidden font-medium md:block">
{item.description}
</div>
<div className="col-span-2 hidden text-right font-mono text-sm md:block">
{item.hours}
</div>
<div className="col-span-2 hidden text-right font-mono text-sm md:block">
{formatCurrency(item.rate)}
</div>
<div className="col-span-1 hidden text-right font-mono font-semibold text-emerald-600 md:block">
{formatCurrency(item.hours * item.rate)}
</div>
<div className="grid gap-6 lg:grid-cols-4 xl:grid-cols-3">
{/* Main Content */}
<div className="space-y-6 lg:col-span-3 xl:col-span-2">
{/* Invoice Header */}
<Card className="shadow-lg">
<CardContent className="p-4 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-3">
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold sm:text-2xl">
Invoice #{invoice.invoiceNumber}
</h1>
<StatusBadge status={getStatusType()} />
</div>
))}
<p className="text-muted-foreground text-sm sm:text-base">
Issued {formatDate(invoice.issueDate)} Due{" "}
{formatDate(invoice.dueDate)}
</p>
</div>
<div className="text-left sm:text-right">
<p className="text-muted-foreground text-sm sm:text-base">
Total Amount
</p>
<p className="text-2xl font-bold text-emerald-600 sm:text-3xl">
{formatCurrency(invoice.totalAmount)}
</p>
</div>
</div>
) : (
<div className="text-muted-foreground py-12 text-center">
<FileText className="mx-auto mb-2 h-8 w-8" />
<p>No line items found</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* Totals & Notes */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Notes */}
{invoice.notes && (
<Card className="border-0 shadow-md lg:col-span-2">
<CardHeader className="pb-4">
<CardTitle className="text-lg">Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground leading-relaxed">
{invoice.notes}
</p>
</CardContent>
</Card>
)}
{/* Totals */}
<Card
className={`border-0 shadow-md ${!invoice.notes ? "lg:col-start-3" : ""}`}
>
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-lg">
<DollarSign className="h-4 w-4 text-emerald-600" />
Total
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal:</span>
<span className="font-mono">{formatCurrency(subtotal)}</span>
</div>
{invoice.taxRate > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Tax ({invoice.taxRate}%):
</span>
<span className="font-mono">{formatCurrency(taxAmount)}</span>
{/* Client & Business Information */}
<div className="grid gap-4 sm:gap-6 md:grid-cols-2">
{/* Client Information */}
<Card className="shadow-lg">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-emerald-600">
<User className="h-4 w-4 sm:h-5 sm:w-5" />
Bill To
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<div>
<h3 className="text-foreground text-lg font-semibold sm:text-xl">
{invoice.client.name}
</h3>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-bold">
<span>Total:</span>
<span className="font-mono text-emerald-600">
{formatCurrency(total)}
</span>
</div>
</div>
{/* Status Actions */}
<div className="pt-2">
{invoice.status === "draft" && (
<Button className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700">
<Send className="mr-2 h-4 w-4" />
Send Invoice
</Button>
)}
<div className="space-y-2 sm:space-y-3">
{invoice.client.email && (
<div className="flex items-center gap-2 sm:gap-3">
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
<Mail className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
</div>
<span className="text-sm break-all sm:text-base">
{invoice.client.email}
</span>
</div>
)}
{invoice.status === "sent" && (
<Button className="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700">
<CheckCircle className="mr-2 h-4 w-4" />
Mark as Paid
</Button>
)}
{invoice.client.phone && (
<div className="flex items-center gap-2 sm:gap-3">
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
<Phone className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
</div>
<span className="text-sm sm:text-base">
{invoice.client.phone}
</span>
</div>
)}
{(invoice.status === "paid" || invoice.status === "overdue") && (
<div className="text-center">
<InvoiceStatusBadge
status={invoice.status}
dueDate={invoice.dueDate}
/>
{(invoice.client.addressLine1 ?? invoice.client.city) && (
<div className="flex items-start gap-2 sm:gap-3">
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
<MapPin className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
</div>
<div className="text-sm sm:text-base">
{invoice.client.addressLine1 && (
<div>{invoice.client.addressLine1}</div>
)}
{invoice.client.addressLine2 && (
<div>{invoice.client.addressLine2}</div>
)}
{(invoice.client.city ??
invoice.client.state ??
invoice.client.postalCode) && (
<div>
{[
invoice.client.city,
invoice.client.state,
invoice.client.postalCode,
]
.filter(Boolean)
.join(", ")}
</div>
)}
{invoice.client.country && (
<div>{invoice.client.country}</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Business Information */}
{invoice.business && (
<Card className="shadow-lg">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-emerald-600">
<Building className="h-4 w-4 sm:h-5 sm:w-5" />
From
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<div>
<h3 className="text-foreground text-lg font-semibold sm:text-xl">
{invoice.business.name}
</h3>
</div>
<div className="space-y-2 sm:space-y-3">
{invoice.business.email && (
<div className="flex items-center gap-2 sm:gap-3">
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
<Mail className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
</div>
<span className="text-sm break-all sm:text-base">
{invoice.business.email}
</span>
</div>
)}
{invoice.business.phone && (
<div className="flex items-center gap-2 sm:gap-3">
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
<Phone className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
</div>
<span className="text-sm sm:text-base">
{invoice.business.phone}
</span>
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
{/* Invoice Items */}
<Card className="shadow-lg">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<InvoiceItemsTable items={invoice.items} />
{/* Totals */}
<div className="mt-6 border-t pt-4">
<div className="flex justify-end">
<div className="w-full space-y-2 sm:max-w-64">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal:</span>
<span className="font-medium">
{formatCurrency(subtotal)}
</span>
</div>
{invoice.taxRate > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Tax ({invoice.taxRate}%):
</span>
<span className="font-medium">
{formatCurrency(taxAmount)}
</span>
</div>
)}
<Separator />
<div className="flex justify-between text-base font-bold sm:text-lg">
<span>Total:</span>
<span className="text-emerald-600">
{formatCurrency(invoice.totalAmount)}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Notes */}
{invoice.notes && (
<Card className="shadow-lg">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Notes
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
{invoice.notes}
</p>
</CardContent>
</Card>
)}
</div>
{/* Sidebar */}
<div className="space-y-4 sm:space-y-6 lg:col-span-1">
{/* Actions */}
<Card className="shadow-lg">
<CardHeader className="pb-3">
<CardTitle className="text-base sm:text-lg">Actions</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<Button
asChild
variant="outline"
className="w-full border-0 shadow-sm"
size="default"
>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<span>Edit Invoice</span>
</Link>
</Button>
<PDFDownloadButton invoiceId={invoice.id} />
<SendInvoiceButton invoiceId={invoice.id} />
<Button
variant="outline"
className="w-full border-0 shadow-sm"
size="default"
>
<Copy className="mr-2 h-4 w-4" />
<span>Duplicate</span>
</Button>
<Button variant="destructive" size="default" className="w-full">
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Invoice</span>
</Button>
</CardContent>
</Card>
{/* Invoice Details */}
<Card className="shadow-lg">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<Calendar className="h-4 w-4 sm:h-5 sm:w-5" />
Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<p className="text-muted-foreground text-sm">Invoice #</p>
<p className="font-medium break-all">
{invoice.invoiceNumber}
</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Status</p>
<div className="mt-1">
<StatusBadge status={getStatusType()} />
</div>
</div>
<div>
<p className="text-muted-foreground text-sm">Issue Date</p>
<p className="font-medium">{formatDate(invoice.issueDate)}</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Due Date</p>
<p className="font-medium">{formatDate(invoice.dueDate)}</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Tax Rate</p>
<p className="font-medium">{invoice.taxRate}%</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Total</p>
<p className="font-medium text-emerald-600">
{formatCurrency(invoice.totalAmount)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
@@ -479,56 +393,35 @@ export default async function InvoicePage({ params }: InvoicePageProps) {
const { id } = await params;
return (
<div className="space-y-6">
<>
<PageHeader
title="Invoice Details"
description="View and manage invoice information"
variant="gradient"
>
<div className="flex items-center gap-2">
<Link href="/dashboard/invoices">
<Button variant="outline" size="sm">
<div className="flex items-center gap-2 sm:gap-3">
<Button
asChild
variant="outline"
className="border-0 shadow-sm"
size="default"
>
<Link href="/dashboard/invoices">
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</Link>
<span className="hidden sm:inline">Back to Invoices</span>
<span className="sm:hidden">Back</span>
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/dashboard/invoices/${id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Send className="mr-2 h-4 w-4" />
Download PDF
</DropdownMenuItem>
<DropdownMenuItem>
<Send className="mr-2 h-4 w-4" />
Send Invoice
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InvoiceActionsDropdown invoiceId={id} />
</div>
</PageHeader>
<HydrateClient>
<Suspense fallback={<div>Loading invoice details...</div>}>
<InvoiceDetails invoiceId={id} />
<Suspense fallback={<InvoiceDetailsSkeleton />}>
<InvoiceContent invoiceId={id} />
</Suspense>
</HydrateClient>
</div>
</>
);
}

View File

@@ -3,10 +3,10 @@
import Link from "next/link";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "~/components/ui/button";
import { StatusBadge, type StatusType } from "~/components/ui/status-badge";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { PDFDownloadButton } from "~/app/dashboard/invoices/[id]/_components/pdf-download-button";
import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table";
import { EmptyState } from "~/components/ui/page-layout";
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table";
import { EmptyState } from "~/components/layout/page-layout";
import { Plus, FileText, Eye, Edit } from "lucide-react";
// Type for invoice data
@@ -182,38 +182,7 @@ const columns: ColumnDef<Invoice>[] = [
</Button>
</Link>
{invoice.items && invoice.client && (
<PDFDownloadButton
invoice={{
id: invoice.id,
invoiceNumber: invoice.invoiceNumber,
issueDate: invoice.issueDate,
dueDate: invoice.dueDate,
status: invoice.status,
totalAmount: invoice.totalAmount,
taxRate: invoice.taxRate,
notes: invoice.notes,
business: invoice.business
? {
name: invoice.business.name,
email: invoice.business.email,
phone: invoice.business.phone,
}
: null,
client: {
name: invoice.client.name,
email: invoice.client.email,
phone: invoice.client.phone,
},
items: invoice.items.map((item) => ({
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.amount,
})),
}}
variant="icon"
/>
<PDFDownloadButton invoiceId={invoice.id} variant="icon" />
)}
</div>
);

View File

@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import {
ArrowLeft,
Upload,

View File

@@ -10,7 +10,7 @@ import { Input } from "~/components/ui/input";
import { NumberInput } from "~/components/ui/number-input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import {
Select,
SelectContent,
@@ -32,7 +32,7 @@ import {
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import { DatePicker } from "~/components/ui/date-picker";
import { FloatingActionBar } from "~/components/ui/floating-action-bar";
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { toast } from "sonner";
import {
ArrowLeft,

View File

@@ -2,10 +2,10 @@ import Link from "next/link";
import { Suspense } from "react";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { PageHeader } from "~/components/page-header";
import { PageHeader } from "~/components/layout/page-header";
import { Plus, Upload } from "lucide-react";
import { InvoicesDataTable } from "./_components/invoices-data-table";
import { DataTableSkeleton } from "~/components/ui/data-table";
import { DataTableSkeleton } from "~/components/data/data-table";
// Invoices Table Component
async function InvoicesTable() {

View File

@@ -1,6 +1,6 @@
import { Navbar } from "~/components/Navbar";
import { Sidebar } from "~/components/Sidebar";
import { DashboardBreadcrumbs } from "~/components/dashboard-breadcrumbs";
import { Navbar } from "~/components/layout/navbar";
import { Sidebar } from "~/components/layout/sidebar";
import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs";
export default function DashboardLayout({
children,

View File

@@ -1,36 +1,258 @@
import { Suspense } from "react";
import { HydrateClient, api } from "~/trpc/server";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { DataTableSkeleton } from "~/components/data/data-table";
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import Link from "next/link";
import {
DashboardStats,
DashboardCards,
DashboardActivity,
} from "./_components/dashboard-components";
import { DashboardPageHeader } from "~/components/page-header";
import { PageContent, PageSection } from "~/components/ui/page-layout";
Users,
FileText,
TrendingUp,
DollarSign,
Plus,
Eye,
Calendar,
ArrowUpRight,
} from "lucide-react";
// Stats Cards Component
async function DashboardStats() {
const [clients, invoices] = await Promise.all([
api.clients.getAll(),
api.invoices.getAll(),
]);
const totalClients = clients.length;
const totalInvoices = invoices.length;
const totalRevenue = invoices.reduce(
(sum, invoice) => sum + invoice.totalAmount,
0,
);
const pendingInvoices = invoices.filter(
(invoice) => invoice.status === "sent" || invoice.status === "draft",
).length;
const stats = [
{
title: "Total Clients",
value: totalClients.toString(),
icon: Users,
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-100 dark:bg-blue-900/20",
},
{
title: "Total Invoices",
value: totalInvoices.toString(),
icon: FileText,
color: "text-emerald-600 dark:text-emerald-400",
bgColor: "bg-emerald-100 dark:bg-emerald-900/20",
},
{
title: "Total Revenue",
value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
icon: DollarSign,
color: "text-teal-600 dark:text-teal-400",
bgColor: "bg-teal-100 dark:bg-teal-900/20",
},
{
title: "Pending Invoices",
value: pendingInvoices.toString(),
icon: Calendar,
color: "text-amber-600 dark:text-amber-400",
bgColor: "bg-amber-100 dark:bg-amber-900/20",
},
];
return (
<Card className="mb-4 border-0 shadow-sm">
<CardContent className="p-4 py-0">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<div key={stat.title} className="flex items-center space-x-3">
<div className={`rounded-lg p-2 ${stat.bgColor}`}>
<Icon className={`h-4 w-4 ${stat.color}`} />
</div>
<div className="min-w-0">
<p className="text-muted-foreground text-xs font-medium">
{stat.title}
</p>
<p className={`text-lg font-bold ${stat.color}`}>
{stat.value}
</p>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
);
}
// Quick Actions Component
function QuickActions() {
return (
<Card className="mb-6 border-0 shadow-sm">
<CardContent className="p-4 py-0">
<div className="flex flex-col gap-3 sm:flex-row sm:gap-4">
<Button
asChild
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-sm hover:from-emerald-700 hover:to-teal-700"
>
<Link href="/dashboard/invoices/new">
<FileText className="mr-2 h-4 w-4" />
Create Invoice
</Link>
</Button>
<Button
asChild
variant="outline"
className="flex-1 border-0 shadow-sm"
>
<Link href="/dashboard/clients/new">
<Users className="mr-2 h-4 w-4" />
Add Client
</Link>
</Button>
<Button
asChild
variant="outline"
className="flex-1 border-0 shadow-sm"
>
<Link href="/dashboard/businesses/new">
<TrendingUp className="mr-2 h-4 w-4" />
Add Business
</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
// Recent Activity Component
async function RecentActivity() {
const invoices = await api.invoices.getAll();
const recentInvoices = invoices
.sort(
(a, b) =>
new Date(b.issueDate).getTime() - new Date(a.issueDate).getTime(),
)
.slice(0, 5);
if (recentInvoices.length === 0) {
return (
<Card className="border-0 shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Recent Activity
</CardTitle>
</CardHeader>
<CardContent>
<div className="py-8 text-center">
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground">
No invoices yet. Create your first invoice to get started!
</p>
<Button
asChild
className="mt-4 bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700"
>
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
Create Invoice
</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
return (
<Card className="border-0 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Calendar className="text-muted-foreground h-5 w-5" />
Recent Activity
</CardTitle>
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard/invoices">
View All
<ArrowUpRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentInvoices.map((invoice) => (
<div
key={invoice.id}
className="hover:bg-muted/50 flex items-center justify-between rounded-lg border p-4 transition-colors"
>
<div className="flex items-center space-x-4">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/20">
<FileText className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<p className="font-medium">
Invoice #{invoice.invoiceNumber}
</p>
<p className="text-muted-foreground text-sm">
{invoice.client?.name} ${invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<StatusBadge status={invoice.status as StatusType} />
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Eye className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
export default async function DashboardPage() {
const session = await auth();
const firstName = session?.user?.name?.split(" ")[0] ?? "User";
return (
<PageContent>
<DashboardPageHeader
title={`Welcome back, ${session?.user?.name?.split(" ")[0] ?? "User"}!`}
description="Here's what's happening with your invoicing business"
<>
<PageHeader
title={`Welcome back, ${firstName}!`}
description="Here's an overview of your invoicing business"
variant="gradient"
/>
<HydrateClient>
<PageSection>
<DashboardStats />
</PageSection>
<div className="space-y-6">
<HydrateClient>
<Suspense fallback={<DataTableSkeleton columns={4} rows={1} />}>
<DashboardStats />
</Suspense>
</HydrateClient>
<PageSection>
<DashboardCards />
</PageSection>
<QuickActions />
<PageSection>
<DashboardActivity />
</PageSection>
</HydrateClient>
</PageContent>
<HydrateClient>
<Suspense fallback={<DataTableSkeleton columns={1} rows={3} />}>
<RecentActivity />
</Suspense>
</HydrateClient>
</div>
</>
);
}

View File

@@ -0,0 +1,532 @@
"use client";
import { useState } from "react";
import * as React from "react";
import { useSession } from "next-auth/react";
import {
Download,
Upload,
User,
Database,
AlertTriangle,
Shield,
Settings,
FileText,
Users,
Building,
} from "lucide-react";
import { api } from "~/trpc/react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { Badge } from "~/components/ui/badge";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
export function SettingsContent() {
const { data: session } = useSession();
const [name, setName] = useState("");
const [deleteConfirmText, setDeleteConfirmText] = useState("");
const [importData, setImportData] = useState("");
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
// Queries
const { data: profile, refetch: refetchProfile } =
api.settings.getProfile.useQuery();
const { data: dataStats } = api.settings.getDataStats.useQuery();
// Mutations
const updateProfileMutation = api.settings.updateProfile.useMutation({
onSuccess: () => {
toast.success("Profile updated successfully");
void refetchProfile();
},
onError: (error: { message: string }) => {
toast.error(`Failed to update profile: ${error.message}`);
},
});
const exportDataQuery = api.settings.exportData.useQuery(undefined, {
enabled: false,
});
// Handle export data success/error
React.useEffect(() => {
if (exportDataQuery.data && !exportDataQuery.isFetching) {
const blob = new Blob([JSON.stringify(exportDataQuery.data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `beenvoice-backup-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success("Data backup downloaded successfully");
}
if (exportDataQuery.error) {
toast.error(`Export failed: ${exportDataQuery.error.message}`);
}
}, [exportDataQuery.data, exportDataQuery.isFetching, exportDataQuery.error]);
const importDataMutation = api.settings.importData.useMutation({
onSuccess: (result) => {
toast.success(
`Data imported successfully! Added ${result.imported.clients} clients, ${result.imported.businesses} businesses, and ${result.imported.invoices} invoices.`,
);
setImportData("");
setIsImportDialogOpen(false);
void refetchProfile();
},
onError: (error: { message: string }) => {
toast.error(`Import failed: ${error.message}`);
},
});
const deleteDataMutation = api.settings.deleteAllData.useMutation({
onSuccess: () => {
toast.success("All data has been permanently deleted");
setDeleteConfirmText("");
},
onError: (error: { message: string }) => {
toast.error(`Delete failed: ${error.message}`);
},
});
const handleUpdateProfile = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
toast.error("Please enter your name");
return;
}
updateProfileMutation.mutate({ name: name.trim() });
};
const handleExportData = () => {
void exportDataQuery.refetch();
};
// Type guard for backup data
const isValidBackupData = (
data: unknown,
): data is {
exportDate: string;
version: string;
user: { name?: string; email: string };
clients: Array<{
name: string;
email?: string;
phone?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
}>;
businesses: Array<{
name: string;
email?: string;
phone?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
website?: string;
taxId?: string;
logoUrl?: string;
isDefault?: boolean;
}>;
invoices: Array<{
invoiceNumber: string;
businessName?: string;
clientName: string;
issueDate: Date;
dueDate: Date;
status?: string;
totalAmount?: number;
taxRate?: number;
notes?: string;
items: Array<{
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
position?: number;
}>;
}>;
} => {
return !!(
data &&
typeof data === "object" &&
data !== null &&
"exportDate" in data &&
"version" in data &&
"user" in data &&
"clients" in data &&
"businesses" in data &&
"invoices" in data
);
};
const handleImportData = () => {
try {
const parsedData: unknown = JSON.parse(importData);
if (isValidBackupData(parsedData)) {
importDataMutation.mutate(parsedData);
} else {
toast.error("Invalid backup file format");
}
} catch {
toast.error("Invalid JSON format. Please check your backup file.");
}
};
const handleDeleteAllData = () => {
if (deleteConfirmText !== "delete all my data") {
toast.error("Please type 'delete all my data' to confirm");
return;
}
deleteDataMutation.mutate({ confirmText: deleteConfirmText });
};
// Set initial name value when profile loads
React.useEffect(() => {
if (profile?.name && !name) {
setName(profile.name);
}
}, [profile?.name, name]);
const dataStatItems = [
{
label: "Clients",
value: dataStats?.clients ?? 0,
icon: Users,
color: "text-blue-600",
bgColor: "bg-blue-100",
},
{
label: "Businesses",
value: dataStats?.businesses ?? 0,
icon: Building,
color: "text-purple-600",
bgColor: "bg-purple-100",
},
{
label: "Invoices",
value: dataStats?.invoices ?? 0,
icon: FileText,
color: "text-emerald-600",
bgColor: "bg-emerald-100",
},
];
return (
<div className="space-y-8">
{/* Profile & Account Overview */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Profile Section */}
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-emerald-100 p-2">
<User className="h-5 w-5 text-emerald-600" />
</div>
Profile Information
</CardTitle>
<CardDescription>
Update your personal account details
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpdateProfile} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your full name"
className="border-0 shadow-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
value={session?.user?.email ?? ""}
disabled
className="bg-muted border-0 shadow-sm"
/>
<p className="text-muted-foreground text-sm">
Email address cannot be changed
</p>
</div>
<Button
type="submit"
disabled={updateProfileMutation.isPending}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"
>
{updateProfileMutation.isPending
? "Updating..."
: "Save Changes"}
</Button>
</form>
</CardContent>
</Card>
{/* Data Overview */}
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-blue-100 p-2">
<Database className="h-5 w-5 text-blue-600" />
</div>
Account Data
</CardTitle>
<CardDescription>
Overview of your stored information
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dataStatItems.map((item) => {
const Icon = item.icon;
return (
<div
key={item.label}
className="flex items-center justify-between rounded-lg border p-3"
>
<div className="flex items-center gap-3">
<div className={`rounded-lg p-2 ${item.bgColor}`}>
<Icon className={`h-4 w-4 ${item.color}`} />
</div>
<span className="font-medium">{item.label}</span>
</div>
<Badge
variant="secondary"
className="text-lg font-semibold"
>
{item.value}
</Badge>
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
{/* Data Management */}
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-indigo-100 p-2">
<Shield className="h-5 w-5 text-indigo-600" />
</div>
Data Management
</CardTitle>
<CardDescription>
Backup, restore, or manage your account data
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="flex gap-4">
<Button
onClick={handleExportData}
disabled={exportDataQuery.isFetching}
variant="outline"
className="flex-1"
>
<Download className="mr-2 h-4 w-4" />
{exportDataQuery.isFetching ? "Exporting..." : "Export Backup"}
</Button>
<Dialog
open={isImportDialogOpen}
onOpenChange={setIsImportDialogOpen}
>
<DialogTrigger asChild>
<Button variant="outline" className="flex-1">
<Upload className="mr-2 h-4 w-4" />
Import Backup
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Import Backup Data</DialogTitle>
<DialogDescription>
Paste the contents of your backup JSON file below. This
will add the data to your existing account.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="Paste your backup JSON data here..."
value={importData}
onChange={(e) => setImportData(e.target.value)}
rows={12}
className="font-mono text-sm"
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsImportDialogOpen(false)}
>
Cancel
</Button>
<Button
onClick={handleImportData}
disabled={
!importData.trim() || importDataMutation.isPending
}
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700"
>
{importDataMutation.isPending
? "Importing..."
: "Import Data"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Backup Information */}
<div className="mt-6 rounded-lg border p-4">
<h4 className="font-medium">Backup Information</h4>
<ul className="text-muted-foreground mt-2 space-y-1 text-sm">
<li> Regular backups protect your important business data</li>
<li> Backup files contain all data in secure JSON format</li>
<li> Import adds to existing data without replacing anything</li>
<li> Store backup files in a secure, accessible location</li>
</ul>
</div>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-red-100 p-2">
<AlertTriangle className="h-5 w-5 text-red-600" />
</div>
Data Management
</CardTitle>
<CardDescription>
Manage your account data with caution
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-lg border p-4">
<h4 className="font-medium text-red-600">
Delete All Account Data
</h4>
<p className="text-muted-foreground mt-2 text-sm">
This will permanently delete all your clients, businesses,
invoices, and related data. This action cannot be undone and
your data cannot be recovered.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-100">
<AlertTriangle className="mr-2 h-4 w-4" />
Delete All Data
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription className="space-y-4">
<div>
This action cannot be undone. This will permanently delete
all your:
</div>
<ul className="list-inside list-disc space-y-1 rounded-lg border p-3 text-sm">
<li>Client information and contact details</li>
<li>Business profiles and settings</li>
<li>Invoices and invoice line items</li>
<li>All related data and records</li>
</ul>
<div className="space-y-2">
<div className="font-medium">
Type{" "}
<span className="bg-muted rounded px-2 py-1 font-mono text-sm">
delete all my data
</span>{" "}
to confirm:
</div>
<Input
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
placeholder="Type delete all my data"
className="font-mono"
/>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteAllData}
disabled={
deleteConfirmText !== "delete all my data" ||
deleteDataMutation.isPending
}
className="bg-red-600 hover:bg-red-700"
>
{deleteDataMutation.isPending
? "Deleting..."
: "Delete Forever"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,502 +1,23 @@
"use client";
import { useState } from "react";
import * as React from "react";
import { useSession } from "next-auth/react";
import {
Download,
Upload,
User,
Database,
AlertTriangle,
Shield,
} from "lucide-react";
import { api } from "~/trpc/react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Textarea } from "~/components/ui/textarea";
import { PageHeader } from "~/components/page-header";
export default function SettingsPage() {
const { data: session } = useSession();
const [name, setName] = useState("");
const [deleteConfirmText, setDeleteConfirmText] = useState("");
const [importData, setImportData] = useState("");
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
// Queries
const { data: profile, refetch: refetchProfile } =
api.settings.getProfile.useQuery();
const { data: dataStats } = api.settings.getDataStats.useQuery();
// Mutations
const updateProfileMutation = api.settings.updateProfile.useMutation({
onSuccess: () => {
toast.success("Your profile has been successfully updated.");
void refetchProfile();
},
onError: (error: { message: string }) => {
toast.error(`Error updating profile: ${error.message}`);
},
});
const exportDataQuery = api.settings.exportData.useQuery(undefined, {
enabled: false,
});
// Handle export data success/error
React.useEffect(() => {
if (exportDataQuery.data && !exportDataQuery.isFetching) {
// Create and download the backup file
const blob = new Blob([JSON.stringify(exportDataQuery.data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `beenvoice-backup-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success("Your data backup has been downloaded.");
}
if (exportDataQuery.error) {
toast.error(`Error exporting data: ${exportDataQuery.error.message}`);
}
}, [exportDataQuery.data, exportDataQuery.isFetching, exportDataQuery.error]);
const importDataMutation = api.settings.importData.useMutation({
onSuccess: (result) => {
toast.success(
`Data imported successfully! Imported ${result.imported.clients} clients, ${result.imported.businesses} businesses, and ${result.imported.invoices} invoices.`,
);
setImportData("");
setIsImportDialogOpen(false);
void refetchProfile();
},
onError: (error: { message: string }) => {
toast.error(`Error importing data: ${error.message}`);
},
});
const deleteDataMutation = api.settings.deleteAllData.useMutation({
onSuccess: () => {
toast.success("Your account data has been permanently deleted.");
setDeleteConfirmText("");
},
onError: (error: { message: string }) => {
toast.error(`Error deleting data: ${error.message}`);
},
});
const handleUpdateProfile = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
toast.error("Please enter your name.");
return;
}
updateProfileMutation.mutate({ name: name.trim() });
};
const handleExportData = () => {
void exportDataQuery.refetch();
};
// Type guard for backup data
const isValidBackupData = (
data: unknown,
): data is {
exportDate: string;
version: string;
user: { name?: string; email: string };
clients: Array<{
name: string;
email?: string;
phone?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
}>;
businesses: Array<{
name: string;
email?: string;
phone?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
website?: string;
taxId?: string;
logoUrl?: string;
isDefault?: boolean;
}>;
invoices: Array<{
invoiceNumber: string;
businessName?: string;
clientName: string;
issueDate: Date;
dueDate: Date;
status?: string;
totalAmount?: number;
taxRate?: number;
notes?: string;
items: Array<{
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
position?: number;
}>;
}>;
} => {
return !!(
data &&
typeof data === "object" &&
data !== null &&
"exportDate" in data &&
"version" in data &&
"user" in data &&
"clients" in data &&
"businesses" in data &&
"invoices" in data
);
};
const handleImportData = () => {
try {
const parsedData: unknown = JSON.parse(importData);
if (isValidBackupData(parsedData)) {
importDataMutation.mutate(parsedData);
} else {
toast.error("Invalid backup file format.");
}
} catch {
toast.error("Invalid JSON. Please check your backup file format.");
}
};
const handleDeleteAllData = () => {
if (deleteConfirmText !== "DELETE ALL DATA") {
toast.error("Please type 'DELETE ALL DATA' to confirm.");
return;
}
deleteDataMutation.mutate({ confirmText: deleteConfirmText });
};
// Set initial name value when profile loads
if (profile && !name && profile.name) {
setName(profile.name);
}
import { Suspense } from "react";
import { HydrateClient } from "~/trpc/server";
import { PageHeader } from "~/components/layout/page-header";
import { DataTableSkeleton } from "~/components/data/data-table";
import { SettingsContent } from "./_components/settings-content";
export default async function SettingsPage() {
return (
<div className="space-y-8">
<>
<PageHeader
title="Settings"
description="Manage your account and data preferences"
variant="large-gradient"
description="Manage your account preferences and data"
variant="gradient"
/>
<div className="grid gap-8 lg:grid-cols-2">
{/* Profile Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5 text-emerald-600" />
Profile
</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleUpdateProfile} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your full name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
value={session?.user?.email ?? ""}
disabled
className="bg-muted"
/>
<p className="text-muted-foreground text-sm">
Email cannot be changed
</p>
</div>
<Button
type="submit"
disabled={updateProfileMutation.isPending}
className="bg-emerald-600 hover:bg-emerald-700"
>
{updateProfileMutation.isPending
? "Updating..."
: "Update Profile"}
</Button>
</form>
</CardContent>
</Card>
{/* Data Statistics */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5 text-emerald-600" />
Your Data
</CardTitle>
<CardDescription>Overview of your account data</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-emerald-600">
{dataStats?.clients ?? 0}
</div>
<div className="text-muted-foreground text-sm">Clients</div>
</div>
<div>
<div className="text-2xl font-bold text-emerald-600">
{dataStats?.businesses ?? 0}
</div>
<div className="text-muted-foreground text-sm">Businesses</div>
</div>
<div>
<div className="text-2xl font-bold text-emerald-600">
{dataStats?.invoices ?? 0}
</div>
<div className="text-muted-foreground text-sm">Invoices</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Backup & Restore Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-emerald-600" />
Backup & Restore
</CardTitle>
<CardDescription>
Export your data for backup or import from a previous backup
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
{/* Export Data */}
<div className="space-y-3">
<h3 className="font-semibold">Export Data</h3>
<p className="text-muted-foreground text-sm">
Download all your clients, businesses, and invoices as a JSON
backup file.
</p>
<Button
onClick={handleExportData}
disabled={exportDataQuery.isFetching}
variant="outline"
className="w-full"
>
<Download className="mr-2 h-4 w-4" />
{exportDataQuery.isFetching ? "Exporting..." : "Export Data"}
</Button>
</div>
{/* Import Data */}
<div className="space-y-3">
<h3 className="font-semibold">Import Data</h3>
<p className="text-muted-foreground text-sm">
Restore your data from a previous backup file.
</p>
<Dialog
open={isImportDialogOpen}
onOpenChange={setIsImportDialogOpen}
>
<DialogTrigger asChild>
<Button variant="outline" className="w-full">
<Upload className="mr-2 h-4 w-4" />
Import Data
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Import Backup Data</DialogTitle>
<DialogDescription>
Paste the contents of your backup JSON file below. This
will add the data to your existing account.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="Paste your backup JSON data here..."
value={importData}
onChange={(e) => setImportData(e.target.value)}
rows={10}
className="font-mono text-sm"
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsImportDialogOpen(false)}
>
Cancel
</Button>
<Button
onClick={handleImportData}
disabled={
!importData.trim() || importDataMutation.isPending
}
className="bg-emerald-600 hover:bg-emerald-700"
>
{importDataMutation.isPending
? "Importing..."
: "Import Data"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<h4 className="font-medium text-blue-900">Backup Tips</h4>
<ul className="mt-2 space-y-1 text-sm text-blue-800">
<li> Regular backups help protect your data</li>
<li>
Backup files contain all your business data in JSON format
</li>
<li>
Import will add data to your existing account (not replace)
</li>
<li> Keep your backup files in a secure location</li>
</ul>
</div>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="border-red-200">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-red-600">
<AlertTriangle className="h-5 w-5" />
Danger Zone
</CardTitle>
<CardDescription>
Irreversible actions for your account data
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<h4 className="font-medium text-red-900">Delete All Data</h4>
<p className="mt-1 text-sm text-red-800">
This will permanently delete all your clients, businesses,
invoices, and related data. This action cannot be undone.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete All Data</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription className="space-y-2">
<p>
This action cannot be undone. This will permanently delete
all your:
</p>
<ul className="list-inside list-disc space-y-1 text-sm">
<li>Clients and their information</li>
<li>Business profiles</li>
<li>Invoices and invoice items</li>
<li>All related data</li>
</ul>
<p className="font-medium">
Type{" "}
<span className="bg-muted rounded px-1 font-mono">
DELETE ALL DATA
</span>{" "}
to confirm:
</p>
<Input
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
placeholder="Type: DELETE ALL DATA"
className="font-mono"
/>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteConfirmText("")}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteAllData}
disabled={
deleteConfirmText !== "DELETE ALL DATA" ||
deleteDataMutation.isPending
}
className="bg-red-600 hover:bg-red-700"
>
{deleteDataMutation.isPending
? "Deleting..."
: "Delete All Data"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
</div>
<HydrateClient>
<Suspense fallback={<DataTableSkeleton columns={1} rows={4} />}>
<SettingsContent />
</Suspense>
</HydrateClient>
</>
);
}

View File

@@ -1,13 +1,13 @@
"use client";
import { useState } from "react";
import { DataTable } from "~/components/ui/data-table";
import { PageHeader } from "~/components/page-header";
import { DataTable } from "~/components/data/data-table";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Plus } from "lucide-react";
import type { ColumnDef } from "@tanstack/react-table";
import { DataTableColumnHeader } from "~/components/ui/data-table";
import { DashboardBreadcrumbs } from "~/components/dashboard-breadcrumbs";
import { DataTableColumnHeader } from "~/components/data/data-table";
import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs";
import Link from "next/link";
// Sample data type

View File

@@ -1,5 +1,5 @@
import { Navbar } from "~/components/Navbar";
import { Sidebar } from "~/components/Sidebar";
import { Navbar } from "~/components/layout/navbar";
import { Sidebar } from "~/components/layout/sidebar";
export default function InvoicesLayout({
children,
@@ -11,10 +11,8 @@ export default function InvoicesLayout({
<Navbar />
<div className="flex">
<Sidebar />
<main className="flex-1 min-h-screen bg-background">
{children}
</main>
<main className="bg-background min-h-screen flex-1">{children}</main>
</div>
</>
);
}
}

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { InvoiceList } from "~/components/invoice-list";
import { InvoiceList } from "~/components/data/invoice-list";
import { Plus } from "lucide-react";
export default async function InvoicesPage() {
@@ -12,8 +12,10 @@ export default async function InvoicesPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view invoices</p>
<h1 className="mb-4 text-4xl font-bold">Access Denied</h1>
<p className="text-muted-foreground mb-8">
Please sign in to view invoices
</p>
<Link href="/api/auth/signin">
<Button size="lg">Sign In</Button>
</Link>
@@ -29,7 +31,7 @@ export default async function InvoicesPage() {
<HydrateClient>
<div className="p-6">
<div className="mb-8">
<h2 className="text-3xl font-bold mb-2">Invoices</h2>
<h2 className="mb-2 text-3xl font-bold">Invoices</h2>
<p className="text-muted-foreground">
Manage your invoices and payments
</p>
@@ -39,4 +41,4 @@ export default async function InvoicesPage() {
</div>
</HydrateClient>
);
}
}

View File

@@ -1,160 +1,244 @@
import Link from "next/link";
import { Button } from "~/components/ui/button";
import { AuthRedirect } from "~/components/AuthRedirect";
import { Card, CardContent } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Logo } from "~/components/branding/logo";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Logo } from "~/components/logo";
import {
Users,
FileText,
DollarSign,
CheckCircle,
ArrowRight,
Star,
Check,
Zap,
Shield,
Globe,
Sparkles,
BarChart3,
Clock,
Rocket,
Heart,
ChevronRight,
} from "lucide-react";
export default function HomePage() {
return (
<div className="bg-gradient-auth min-h-screen">
<div className="bg-background min-h-screen">
<AuthRedirect />
{/* Header */}
<header className="border-border bg-card/80 border-b backdrop-blur-sm">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
{/* Navigation */}
<nav className="bg-background/80 sticky top-0 z-50 border-b backdrop-blur-xl">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
<Logo />
<div className="flex items-center space-x-4">
<div className="hidden items-center space-x-8 md:flex">
<a
href="#features"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Features
</a>
<a
href="#pricing"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Pricing
</a>
</div>
<div className="flex items-center space-x-3">
<Link href="/auth/signin">
<Button variant="ghost">Sign In</Button>
</Link>
<Link href="/auth/register">
<Button>Get Started</Button>
<Button className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-lg shadow-emerald-500/25 transition-all duration-300 hover:shadow-xl hover:shadow-emerald-500/30">
Get Started Free
</Button>
</Link>
</div>
</div>
</div>
</header>
</nav>
{/* Hero Section */}
<section className="px-4 py-20">
<div className="container mx-auto max-w-4xl text-center">
<h1 className="text-foreground mb-6 text-5xl font-bold md:text-6xl">
Simple Invoicing for
<span className="text-green-600"> Freelancers</span>
</h1>
<p className="text-muted-foreground mx-auto mb-8 max-w-2xl text-xl">
Create professional invoices, manage clients, and get paid faster
with beenvoice. The invoicing app that works as hard as you do.
</p>
<div className="flex flex-col justify-center gap-4 sm:flex-row">
<Link href="/auth/register">
<Button size="lg" className="px-8 py-6 text-lg">
Start Free Trial
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
<Link href="#features">
<Button variant="outline" size="lg" className="px-8 py-6 text-lg">
See How It Works
</Button>
</Link>
<section className="relative overflow-hidden pt-20 pb-16">
<div className="relative container mx-auto px-4 text-center">
<div className="mx-auto max-w-4xl">
<Badge
variant="secondary"
className="mb-6 border-emerald-200 bg-emerald-100 text-emerald-800"
>
<Sparkles className="mr-1 h-3 w-3" />
100% Free Forever
</Badge>
<h1 className="text-foreground mb-6 text-6xl font-bold tracking-tight sm:text-7xl lg:text-8xl">
Simple Invoicing for
<span className="block text-emerald-600">Freelancers</span>
</h1>
<p className="text-muted-foreground mx-auto mb-8 max-w-2xl text-xl leading-relaxed">
Create professional invoices, manage clients, and track payments.
Built specifically for freelancers and small businesses
<span className="font-semibold text-emerald-600">
completely free
</span>
.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Link href="/auth/register">
<Button
size="lg"
className="group bg-gradient-to-r from-emerald-600 to-teal-600 px-8 py-4 text-lg font-semibold shadow-xl shadow-emerald-500/25 transition-all duration-300 hover:shadow-2xl hover:shadow-emerald-500/30"
>
Start Free
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
<Link href="#demo">
<Button
variant="outline"
size="lg"
className="group border-slate-300 px-8 py-4 text-lg hover:border-slate-400 hover:bg-slate-50"
>
See Features
<ChevronRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</div>
<div className="mt-12 flex items-center justify-center gap-8 text-sm text-slate-500">
{[
"No credit card required",
"Setup in 2 minutes",
"Cancel anytime",
].map((text, i) => (
<div key={i} className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
{text}
</div>
))}
</div>
</div>
</div>
</section>
{/* Stats */}
<section className="bg-muted/50 border-y py-12">
<div className="container mx-auto px-4">
<div className="text-center">
<p className="text-muted-foreground">
Free invoicing for independent professionals
</p>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="bg-card px-4 py-20">
<div className="container mx-auto max-w-6xl">
<section id="features" className="py-24">
<div className="container mx-auto px-4">
<div className="mb-16 text-center">
<h2 className="text-card-foreground mb-4 text-4xl font-bold">
Everything you need to invoice like a pro
<Badge
variant="secondary"
className="mb-4 border-blue-200 bg-blue-100 text-blue-800"
>
<Zap className="mr-1 h-3 w-3" />
Supercharged Features
</Badge>
<h2 className="text-foreground mb-4 text-5xl font-bold tracking-tight">
Everything you need to
<span className="block text-emerald-600">
invoice professionally
</span>
</h2>
<p className="text-muted-foreground mx-auto max-w-2xl text-xl">
Powerful features designed for freelancers and small businesses
Simple, powerful features designed specifically for freelancers
and small businesses.
</p>
</div>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<Card className="border-0 shadow-lg">
<CardHeader>
<Users className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Client Management</CardTitle>
<CardDescription>
Keep all your client information organized in one place
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-8 lg:grid-cols-3">
{/* Feature 1 */}
<Card className="group shadow-lg transition-all duration-300 hover:shadow-xl">
<CardContent className="p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500 text-white">
<Rocket className="h-6 w-6" />
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Quick Setup
</h3>
<p className="text-muted-foreground mb-4">
Start creating invoices immediately. No complicated setup or
configuration required.
</p>
<ul className="text-muted-foreground space-y-2 text-sm">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Store contact details and addresses
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Simple client management
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Track client history and invoices
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Professional templates
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Search and filter clients easily
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Easy invoice sending
</li>
</ul>
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<CardHeader>
<FileText className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Professional Invoices</CardTitle>
<CardDescription>
Create beautiful, detailed invoices with line items
</CardDescription>
</CardHeader>
<CardContent>
{/* Feature 2 */}
<Card className="group shadow-lg transition-all duration-300 hover:shadow-xl">
<CardContent className="p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500 text-white">
<BarChart3 className="h-6 w-6" />
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Payment Tracking
</h3>
<p className="text-muted-foreground mb-4">
Keep track of invoice status and monitor which clients have
paid.
</p>
<ul className="text-muted-foreground space-y-2 text-sm">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Add multiple line items with dates
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Invoice status tracking
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Automatic calculations and totals
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Payment history
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Professional invoice numbering
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Overdue notifications
</li>
</ul>
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<CardHeader>
<DollarSign className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Payment Tracking</CardTitle>
<CardDescription>
Monitor invoice status and track payments
</CardDescription>
</CardHeader>
<CardContent>
{/* Feature 3 */}
<Card className="group shadow-lg transition-all duration-300 hover:shadow-xl">
<CardContent className="p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-purple-500 text-white">
<Globe className="h-6 w-6" />
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Professional Features
</h3>
<p className="text-muted-foreground mb-4">
Everything you need to look professional and get paid on time.
</p>
<ul className="text-muted-foreground space-y-2 text-sm">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Track draft, sent, paid, and overdue status
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
PDF generation
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
View outstanding amounts at a glance
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Custom tax rates
</li>
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Payment history and analytics
<li className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" />
Professional numbering
</li>
</ul>
</CardContent>
@@ -163,115 +247,208 @@ export default function HomePage() {
</div>
</section>
{/* Benefits Section */}
<section className="bg-muted/50 px-4 py-20">
<div className="container mx-auto max-w-4xl text-center">
<h2 className="text-foreground mb-16 text-4xl font-bold">
Why choose beenvoice?
</h2>
{/* Pricing Section */}
<section id="pricing" className="bg-muted/50 py-24">
<div className="container mx-auto px-4">
<div className="mb-16 text-center">
<h2 className="text-foreground mb-4 text-5xl font-bold tracking-tight">
Simple, transparent pricing
</h2>
<p className="text-muted-foreground mx-auto max-w-2xl text-xl">
Start free, stay free. No hidden fees, no gotchas, no limits on
your success.
</p>
</div>
<div className="grid gap-12 md:grid-cols-2">
<div className="space-y-6">
<div className="flex items-start space-x-4">
<Zap className="mt-1 h-8 w-8 text-green-600" />
<div className="text-left">
<h3 className="text-foreground mb-2 text-xl font-semibold">
Lightning Fast
</h3>
<p className="text-muted-foreground">
Create invoices in seconds, not minutes. Our streamlined
interface gets you back to work faster.
</p>
</div>
<div className="mx-auto max-w-md">
<Card className="bg-card relative border-2 border-emerald-500 shadow-2xl">
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<Badge className="bg-emerald-500 px-6 py-1 text-white">
Forever Free
</Badge>
</div>
<CardContent className="p-8 text-center">
<div className="mb-6">
<div className="text-foreground mb-2 text-6xl font-bold">
$0
</div>
<div className="text-muted-foreground">
per month, forever
</div>
</div>
<div className="flex items-start space-x-4">
<Shield className="mt-1 h-8 w-8 text-green-600" />
<div className="text-left">
<h3 className="text-foreground mb-2 text-xl font-semibold">
Secure & Private
</h3>
<p className="text-muted-foreground">
Your data is encrypted and secure. We never share your
information with third parties.
</p>
<div className="mb-8 space-y-4 text-left">
{[
"Unlimited invoices",
"Unlimited clients",
"Professional templates",
"PDF export",
"Payment tracking",
"Multi-business support",
"Line item details",
"Free forever",
].map((feature, i) => (
<div key={i} className="flex items-center gap-3">
<Check className="h-5 w-5 flex-shrink-0 text-emerald-500" />
<span className="text-foreground">{feature}</span>
</div>
))}
</div>
<Link href="/auth/register">
<Button className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 py-3 text-lg font-semibold shadow-lg shadow-emerald-500/25 transition-all duration-300 hover:shadow-xl hover:shadow-emerald-500/30">
Get Started Now
</Button>
</Link>
<p className="text-muted-foreground mt-4 text-sm">
No credit card required
</p>
</CardContent>
</Card>
</div>
</div>
</section>
{/* Why Choose */}
<section className="py-24">
<div className="container mx-auto px-4">
<div className="mb-16 text-center">
<h2 className="text-foreground mb-4 text-5xl font-bold tracking-tight">
Why freelancers
<span className="block text-emerald-600">choose BeenVoice</span>
</h2>
</div>
<div className="grid gap-8 md:grid-cols-3">
<div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500 text-white">
<Zap className="h-6 w-6" />
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Quick & Simple
</h3>
<p className="text-muted-foreground">
No learning curve. Start creating professional invoices in
minutes, not hours.
</p>
</div>
<div className="space-y-6">
<div className="flex items-start space-x-4">
<Star className="mt-1 h-8 w-8 text-green-600" />
<div className="text-left">
<h3 className="text-foreground mb-2 text-xl font-semibold">
Professional Quality
</h3>
<p className="text-muted-foreground">
Generate invoices that look professional and build trust
with your clients.
</p>
</div>
<div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500 text-white">
<Shield className="h-6 w-6" />
</div>
<div className="flex items-start space-x-4">
<Clock className="mt-1 h-8 w-8 text-green-600" />
<div className="text-left">
<h3 className="text-foreground mb-2 text-xl font-semibold">
Save Time
</h3>
<p className="text-muted-foreground">
Automated calculations, templates, and client management
save you hours every month.
</p>
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Always Free
</h3>
<p className="text-muted-foreground">
No hidden fees, no premium tiers. All features are free for as
long as you need them.
</p>
</div>
<div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-purple-500 text-white">
<Clock className="h-6 w-6" />
</div>
<h3 className="text-foreground mb-3 text-xl font-bold">
Save Time
</h3>
<p className="text-muted-foreground">
Focus on your work, not paperwork. Automated calculations and
professional formatting.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-green-600 px-4 py-20">
<div className="container mx-auto max-w-2xl text-center">
<h2 className="mb-4 text-4xl font-bold text-white">
Ready to get started?
</h2>
<p className="mb-8 text-xl text-green-100">
Join thousands of freelancers who trust beenvoice for their
invoicing needs.
</p>
<Link href="/auth/register">
<Button size="lg" variant="secondary" className="px-8 py-6 text-lg">
Start Your Free Trial
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
<p className="mt-4 text-sm text-green-200">
No credit card required Cancel anytime
</p>
<section className="relative overflow-hidden bg-gradient-to-br from-emerald-600 via-emerald-700 to-teal-800 py-24">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-600/90 to-teal-800/90"></div>
<div className="absolute top-10 left-10 h-64 w-64 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute right-10 bottom-10 h-80 w-80 rounded-full bg-white/5 blur-3xl"></div>
<div className="relative container mx-auto px-4 text-center">
<div className="mx-auto max-w-3xl">
<h2 className="mb-6 text-5xl font-bold text-white">
Ready to revolutionize
<span className="block">your invoicing?</span>
</h2>
<p className="mb-8 text-xl text-emerald-100">
Join thousands of entrepreneurs who&apos;ve already transformed
their business with BeenVoice. Start your journey
today&mdash;completely free.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Link href="/auth/register">
<Button
size="lg"
variant="secondary"
className="group bg-white px-8 py-4 text-lg font-semibold text-emerald-700 shadow-xl transition-all duration-300 hover:bg-gray-50 hover:shadow-2xl"
>
Start Your Success Story
<Rocket className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</div>
<div className="mt-8 flex items-center justify-center gap-8 text-emerald-200">
<div className="flex items-center gap-2">
<Heart className="h-4 w-4" />
Free forever
</div>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Secure & private
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
2-minute setup
</div>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-card text-card-foreground border-border border-t px-4 py-12">
<div className="container mx-auto text-center">
<Logo className="mx-auto mb-4" />
<p className="text-muted-foreground mb-4">
Simple invoicing for freelancers and small businesses
</p>
<div className="text-muted-foreground flex justify-center space-x-6 text-sm">
<Link
href="/auth/signin"
className="hover:text-foreground transition-colors"
>
Sign In
</Link>
<Link
href="/auth/register"
className="hover:text-foreground transition-colors"
>
Register
</Link>
<footer className="bg-background border-t py-12">
<div className="container mx-auto px-4">
<div className="text-center">
<Logo className="mx-auto mb-4" />
<p className="text-muted-foreground mb-6">
Simple invoicing for freelancers. Free, forever.
</p>
<div className="text-muted-foreground flex items-center justify-center gap-8 text-sm">
<Link
href="/auth/signin"
className="hover:text-foreground transition-colors"
>
Sign In
</Link>
<Link
href="/auth/register"
className="hover:text-foreground transition-colors"
>
Get Started
</Link>
<a
href="#features"
className="hover:text-foreground transition-colors"
>
Features
</a>
<a
href="#pricing"
className="hover:text-foreground transition-colors"
>
Pricing
</a>
</div>
<div className="mt-8 border-t pt-8">
<p className="text-muted-foreground">
&copy; 2024 BeenVoice. Built with &hearts; for entrepreneurs.
</p>
</div>
</div>
</div>
</footer>

View File

@@ -24,7 +24,7 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { FileUpload } from "~/components/ui/file-upload";
import { FileUpload } from "~/components/forms/file-upload";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Progress } from "~/components/ui/progress";

View File

@@ -21,7 +21,7 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { StatusBadge, type StatusType } from "~/components/ui/status-badge";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { toast } from "sonner";
import {
FileText,

View File

@@ -6,7 +6,7 @@ import { api } from "~/trpc/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { StatusBadge, type StatusType } from "~/components/ui/status-badge";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { Separator } from "~/components/ui/separator";
import {
Dialog,

View File

@@ -1,15 +1,27 @@
import * as React from "react";
import { Badge, type badgeVariants } from "./badge";
import { Badge, type badgeVariants } from "~/components/ui/badge";
import { type VariantProps } from "class-variance-authority";
type StatusType = "draft" | "sent" | "paid" | "overdue" | "success" | "warning" | "error" | "info";
type StatusType =
| "draft"
| "sent"
| "paid"
| "overdue"
| "success"
| "warning"
| "error"
| "info";
interface StatusBadgeProps extends Omit<React.ComponentProps<typeof Badge>, "variant"> {
interface StatusBadgeProps
extends Omit<React.ComponentProps<typeof Badge>, "variant"> {
status: StatusType;
children?: React.ReactNode;
}
const statusVariantMap: Record<StatusType, VariantProps<typeof badgeVariants>["variant"]> = {
const statusVariantMap: Record<
StatusType,
VariantProps<typeof badgeVariants>["variant"]
> = {
draft: "secondary",
sent: "info",
paid: "success",

View File

@@ -22,8 +22,8 @@ import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { FormSkeleton } from "~/components/ui/skeleton";
import { Switch } from "~/components/ui/switch";
import { AddressForm } from "~/components/ui/address-form";
import { FloatingActionBar } from "~/components/ui/floating-action-bar";
import { AddressForm } from "~/components/forms/address-form";
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { api } from "~/trpc/react";
import {
formatPhoneNumber,

View File

@@ -10,8 +10,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { FormSkeleton } from "~/components/ui/skeleton";
import { AddressForm } from "~/components/ui/address-form";
import { FloatingActionBar } from "~/components/ui/floating-action-bar";
import { AddressForm } from "~/components/forms/address-form";
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { api } from "~/trpc/react";
import {
formatPhoneNumber,

View File

@@ -5,7 +5,7 @@ import { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cn } from "~/lib/utils";
import { Upload, FileText, X, CheckCircle, AlertCircle } from "lucide-react";
import { Button } from "./button";
import { Button } from "~/components/ui/button";
interface FileUploadProps {
onFilesSelected: (files: File[]) => void;
@@ -25,7 +25,12 @@ interface FilePreviewProps {
error?: string;
}
function FilePreview({ file, onRemove, status = "pending", error }: FilePreviewProps) {
function FilePreview({
file,
onRemove,
status = "pending",
error,
}: FilePreviewProps) {
const getStatusIcon = () => {
switch (status) {
case "success":
@@ -49,20 +54,22 @@ function FilePreview({ file, onRemove, status = "pending", error }: FilePreviewP
};
return (
<div className={cn(
"flex items-center justify-between p-3 rounded-lg border",
getStatusColor()
)}>
<div
className={cn(
"flex items-center justify-between rounded-lg border p-3",
getStatusColor(),
)}
>
<div className="flex items-center gap-3">
{getStatusIcon()}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{file.name}</p>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
{file.name}
</p>
<p className="text-xs text-gray-500">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
{error && (
<p className="text-xs text-red-600 mt-1">{error}</p>
)}
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
</div>
</div>
<Button
@@ -85,99 +92,111 @@ export function FileUpload({
className,
disabled = false,
placeholder = "Drag & drop files here, or click to select",
description
description,
}: FileUploadProps) {
const [files, setFiles] = React.useState<File[]>([]);
const [errors, setErrors] = React.useState<Record<string, string>>({});
const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: any[]) => {
// Handle accepted files
const newFiles = [...files, ...acceptedFiles];
setFiles(newFiles);
onFilesSelected(newFiles);
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: any[]) => {
// Handle accepted files
const newFiles = [...files, ...acceptedFiles];
setFiles(newFiles);
onFilesSelected(newFiles);
// Handle rejected files
const newErrors: Record<string, string> = { ...errors };
rejectedFiles.forEach(({ file, errors }) => {
const errorMessage = errors.map((e: any) => {
if (e.code === 'file-too-large') {
return `File is too large. Max size is ${(maxSize / 1024 / 1024).toFixed(1)}MB`;
}
if (e.code === 'file-invalid-type') {
return 'File type not supported';
}
if (e.code === 'too-many-files') {
return `Too many files. Max is ${maxFiles}`;
}
return e.message;
}).join(', ');
newErrors[file.name] = errorMessage;
});
setErrors(newErrors);
}, [files, onFilesSelected, errors, maxFiles, maxSize]);
// Handle rejected files
const newErrors: Record<string, string> = { ...errors };
rejectedFiles.forEach(({ file, errors }) => {
const errorMessage = errors
.map((e: any) => {
if (e.code === "file-too-large") {
return `File is too large. Max size is ${(maxSize / 1024 / 1024).toFixed(1)}MB`;
}
if (e.code === "file-invalid-type") {
return "File type not supported";
}
if (e.code === "too-many-files") {
return `Too many files. Max is ${maxFiles}`;
}
return e.message;
})
.join(", ");
newErrors[file.name] = errorMessage;
});
setErrors(newErrors);
},
[files, onFilesSelected, errors, maxFiles, maxSize],
);
const removeFile = (fileToRemove: File) => {
const newFiles = files.filter(file => file !== fileToRemove);
const newFiles = files.filter((file) => file !== fileToRemove);
setFiles(newFiles);
onFilesSelected(newFiles);
const newErrors = { ...errors };
delete newErrors[fileToRemove.name];
setErrors(newErrors);
};
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
onDrop,
accept,
maxFiles,
maxSize,
disabled
});
const { getRootProps, getInputProps, isDragActive, isDragReject } =
useDropzone({
onDrop,
accept,
maxFiles,
maxSize,
disabled,
});
return (
<div className={cn("space-y-4", className)}>
<div
{...getRootProps()}
className={cn(
"border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer",
"cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors",
"hover:border-emerald-400 hover:bg-emerald-50/50",
isDragActive && "border-emerald-400 bg-emerald-50/50",
isDragReject && "border-red-400 bg-red-50/50",
disabled && "opacity-50 cursor-not-allowed",
"bg-white/80 backdrop-blur-sm"
disabled && "cursor-not-allowed opacity-50",
"bg-white/80 backdrop-blur-sm",
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center gap-4">
<div className={cn(
"p-3 rounded-full transition-colors",
isDragActive ? "bg-emerald-100" : "bg-gray-100",
isDragReject && "bg-red-100"
)}>
<Upload className={cn(
"h-6 w-6 transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-400",
isDragReject && "text-red-600"
)} />
<div
className={cn(
"rounded-full p-3 transition-colors",
isDragActive ? "bg-emerald-100" : "bg-gray-100",
isDragReject && "bg-red-100",
)}
>
<Upload
className={cn(
"h-6 w-6 transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-400",
isDragReject && "text-red-600",
)}
/>
</div>
<div className="space-y-2">
<p className={cn(
"text-lg font-medium transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-900",
isDragReject && "text-red-600"
)}>
{isDragActive
? isDragReject
? "File type not supported"
<p
className={cn(
"text-lg font-medium transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-900",
isDragReject && "text-red-600",
)}
>
{isDragActive
? isDragReject
? "File type not supported"
: "Drop files here"
: placeholder
}
: placeholder}
</p>
{description && (
<p className="text-sm text-gray-500">{description}</p>
)}
<p className="text-xs text-gray-400">
Max {maxFiles} file{maxFiles !== 1 ? 's' : ''} {(maxSize / 1024 / 1024).toFixed(1)}MB each
Max {maxFiles} file{maxFiles !== 1 ? "s" : ""} {" "}
{(maxSize / 1024 / 1024).toFixed(1)}MB each
</p>
</div>
</div>
@@ -187,7 +206,7 @@ export function FileUpload({
{files.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Selected Files</h4>
<div className="space-y-2 max-h-60 overflow-y-auto">
<div className="max-h-60 space-y-2 overflow-y-auto">
{files.map((file, index) => (
<FilePreview
key={`${file.name}-${index}`}
@@ -203,16 +222,20 @@ export function FileUpload({
{/* Error Summary */}
{Object.keys(errors).length > 0 && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800">Upload Errors</span>
<span className="text-sm font-medium text-red-800">
Upload Errors
</span>
</div>
<ul className="text-sm text-red-700 space-y-1">
<ul className="space-y-1 text-sm text-red-700">
{Object.entries(errors).map(([fileName, error]) => (
<li key={fileName} className="flex items-start gap-2">
<span className="text-red-600"></span>
<span><strong>{fileName}:</strong> {error}</span>
<span>
<strong>{fileName}:</strong> {error}
</span>
</li>
))}
</ul>
@@ -220,4 +243,4 @@ export function FileUpload({
)}
</div>
);
}
}

View File

@@ -36,7 +36,7 @@ import {
import { useRouter } from "next/navigation";
import { format } from "date-fns";
import { FormSkeleton } from "~/components/ui/skeleton";
import { EditableInvoiceItems } from "~/components/editable-invoice-items";
import { EditableInvoiceItems } from "~/components/data/editable-invoice-items";
const STATUS_OPTIONS = [
{
@@ -273,16 +273,16 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
return (
<div className="space-y-6 pb-20">
{/* Invoice Details Card Skeleton */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Card className="shadow-lg">
<CardHeader>
<div className="h-6 w-48 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-6 w-48 animate-pulse rounded bg-gray-300"></div>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="h-4 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-10 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-4 w-24 animate-pulse rounded bg-gray-300"></div>
<div className="h-10 animate-pulse rounded bg-gray-300"></div>
</div>
))}
</div>
@@ -290,20 +290,20 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card>
{/* Invoice Items Card Skeleton */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Card className="shadow-lg">
<CardHeader>
<div className="flex items-center justify-between">
<div className="h-6 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-10 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-6 w-32 animate-pulse rounded bg-gray-300"></div>
<div className="h-10 w-24 animate-pulse rounded bg-gray-300"></div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Items Table Header Skeleton */}
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3 dark:bg-gray-700">
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="h-4 animate-pulse rounded bg-gray-300 dark:bg-gray-600"
className="h-4 animate-pulse rounded bg-gray-300"
></div>
))}
</div>
@@ -313,7 +313,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4 dark:border-gray-700"
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4"
>
{Array.from({ length: 8 }).map((_, j) => (
<div
@@ -353,7 +353,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
return (
<div className="space-y-6 pb-20">
{/* Invoice Details Card Skeleton */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="shadow-lg">
<CardHeader>
<div className="h-6 w-48 animate-pulse rounded bg-gray-300"></div>
</CardHeader>
@@ -370,7 +370,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card>
{/* Invoice Items Card Skeleton */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Card className="shadow-lg">
<CardHeader>
<div className="flex items-center justify-between">
<div className="h-6 w-32 animate-pulse rounded bg-gray-300"></div>
@@ -423,9 +423,9 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
return (
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6 pb-20">
{/* Invoice Details Card */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="flex items-center gap-2 text-emerald-700">
<FileText className="h-5 w-5" />
Invoice Details
</CardTitle>
@@ -653,10 +653,10 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card>
{/* Invoice Items Card */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Card className="shadow-lg">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="flex items-center gap-2 text-emerald-700">
<Clock className="h-5 w-5" />
Invoice Items
</CardTitle>

View File

@@ -4,8 +4,8 @@ import { useSession, signOut } from "next-auth/react";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { Skeleton } from "~/components/ui/skeleton";
import { Logo } from "./logo";
import { SidebarTrigger } from "./SidebarTrigger";
import { Logo } from "~/components/branding/logo";
import { SidebarTrigger } from "~/components/navigation/sidebar-trigger";
export function Navbar() {
const { data: session, status } = useSession();

View File

@@ -1,7 +1,12 @@
import { z } from "zod";
import { eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { invoices, invoiceItems, clients, businesses } from "~/server/db/schema";
import {
invoices,
invoiceItems,
clients,
businesses,
} from "~/server/db/schema";
import { TRPCError } from "@trpc/server";
const invoiceItemSchema = z.object({
@@ -35,15 +40,15 @@ const updateStatusSchema = z.object({
export const invoicesRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
try {
return await ctx.db.query.invoices.findMany({
where: eq(invoices.createdById, ctx.session.user.id),
with: {
business: true,
client: true,
items: true,
},
orderBy: (invoices, { desc }) => [desc(invoices.createdAt)],
});
return await ctx.db.query.invoices.findMany({
where: eq(invoices.createdById, ctx.session.user.id),
with: {
business: true,
client: true,
items: true,
},
orderBy: (invoices, { desc }) => [desc(invoices.issueDate)],
});
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@@ -58,11 +63,11 @@ export const invoicesRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
try {
const invoice = await ctx.db.query.invoices.findFirst({
where: eq(invoices.id, input.id),
with: {
business: true,
client: true,
items: {
where: eq(invoices.id, input.id),
with: {
business: true,
client: true,
items: {
orderBy: (items, { asc }) => [asc(items.position)],
},
},
@@ -90,7 +95,7 @@ export const invoicesRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch invoice",
cause: error,
});
});
}
}),
@@ -98,8 +103,8 @@ export const invoicesRouter = createTRPCRouter({
.input(createInvoiceSchema)
.mutation(async ({ ctx, input }) => {
try {
const { items, ...invoiceData } = input;
const { items, ...invoiceData } = input;
// Verify business exists and belongs to user (if provided)
if (invoiceData.businessId) {
const business = await ctx.db.query.businesses.findFirst({
@@ -116,11 +121,12 @@ export const invoicesRouter = createTRPCRouter({
if (business.createdById !== ctx.session.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to create invoices for this business",
message:
"You don't have permission to create invoices for this business",
});
}
}
// Verify client exists and belongs to user
const client = await ctx.db.query.clients.findFirst({
where: eq(clients.id, invoiceData.clientId),
@@ -136,40 +142,47 @@ export const invoicesRouter = createTRPCRouter({
if (client.createdById !== ctx.session.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to create invoices for this client",
message:
"You don't have permission to create invoices for this client",
});
}
// Calculate subtotal and tax
const subtotal = items.reduce((sum, item) => sum + (item.hours * item.rate), 0);
const taxAmount = (subtotal * invoiceData.taxRate) / 100;
const totalAmount = subtotal + taxAmount;
// Create invoice
const [invoice] = await ctx.db.insert(invoices).values({
...invoiceData,
totalAmount,
createdById: ctx.session.user.id,
}).returning();
// Calculate subtotal and tax
const subtotal = items.reduce(
(sum, item) => sum + item.hours * item.rate,
0,
);
const taxAmount = (subtotal * invoiceData.taxRate) / 100;
const totalAmount = subtotal + taxAmount;
if (!invoice) {
// Create invoice
const [invoice] = await ctx.db
.insert(invoices)
.values({
...invoiceData,
totalAmount,
createdById: ctx.session.user.id,
})
.returning();
if (!invoice) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create invoice",
});
}
}
// Create invoice items
// Create invoice items
const itemsToInsert = items.map((item, idx) => ({
...item,
invoiceId: invoice.id,
amount: item.hours * item.rate,
...item,
invoiceId: invoice.id,
amount: item.hours * item.rate,
position: idx,
}));
}));
await ctx.db.insert(invoiceItems).values(itemsToInsert);
await ctx.db.insert(invoiceItems).values(itemsToInsert);
return invoice;
return invoice;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
@@ -184,8 +197,8 @@ export const invoicesRouter = createTRPCRouter({
.input(updateInvoiceSchema)
.mutation(async ({ ctx, input }) => {
try {
const { id, items, ...invoiceData } = input;
const { id, items, ...invoiceData } = input;
// Verify invoice exists and belongs to user
const existingInvoice = await ctx.db.query.invoices.findFirst({
where: eq(invoices.id, id),
@@ -232,46 +245,52 @@ export const invoicesRouter = createTRPCRouter({
});
}
}
if (items) {
// Calculate subtotal and tax
const subtotal = items.reduce((sum, item) => sum + (item.hours * item.rate), 0);
const taxAmount = (subtotal * (invoiceData.taxRate ?? existingInvoice.taxRate)) / 100;
const totalAmount = subtotal + taxAmount;
// Update invoice
await ctx.db
.update(invoices)
.set({
...invoiceData,
totalAmount,
updatedAt: new Date(),
})
.where(eq(invoices.id, id));
// Delete existing items and create new ones
await ctx.db.delete(invoiceItems).where(eq(invoiceItems.invoiceId, id));
if (items) {
// Calculate subtotal and tax
const subtotal = items.reduce(
(sum, item) => sum + item.hours * item.rate,
0,
);
const taxAmount =
(subtotal * (invoiceData.taxRate ?? existingInvoice.taxRate)) / 100;
const totalAmount = subtotal + taxAmount;
// Update invoice
await ctx.db
.update(invoices)
.set({
...invoiceData,
totalAmount,
updatedAt: new Date(),
})
.where(eq(invoices.id, id));
// Delete existing items and create new ones
await ctx.db
.delete(invoiceItems)
.where(eq(invoiceItems.invoiceId, id));
const itemsToInsert = items.map((item, idx) => ({
...item,
invoiceId: id,
amount: item.hours * item.rate,
...item,
invoiceId: id,
amount: item.hours * item.rate,
position: idx,
}));
}));
await ctx.db.insert(invoiceItems).values(itemsToInsert);
} else {
// Update invoice without items
await ctx.db
.update(invoices)
.set({
...invoiceData,
updatedAt: new Date(),
})
.where(eq(invoices.id, id));
}
await ctx.db.insert(invoiceItems).values(itemsToInsert);
} else {
// Update invoice without items
await ctx.db
.update(invoices)
.set({
...invoiceData,
updatedAt: new Date(),
})
.where(eq(invoices.id, id));
}
return { success: true };
return { success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
@@ -305,9 +324,9 @@ export const invoicesRouter = createTRPCRouter({
});
}
// Items will be deleted automatically due to cascade
// Items will be deleted automatically due to cascade
await ctx.db.delete(invoices).where(eq(invoices.id, input.id));
return { success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
@@ -343,12 +362,12 @@ export const invoicesRouter = createTRPCRouter({
}
await ctx.db
.update(invoices)
.set({
status: input.status,
updatedAt: new Date(),
})
.where(eq(invoices.id, input.id));
.update(invoices)
.set({
status: input.status,
updatedAt: new Date(),
})
.where(eq(invoices.id, input.id));
return { success: true };
} catch (error) {
@@ -360,4 +379,4 @@ export const invoicesRouter = createTRPCRouter({
});
}
}),
});
});

View File

@@ -107,19 +107,19 @@
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.145 0 0);
--background: oklch(0.145 0.02 160);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card: oklch(0.205 0.02 160);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover: oklch(0.205 0.02 160);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary: oklch(0.269 0.015 160);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted: oklch(0.269 0.015 160);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent: oklch(0.269 0.015 160);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
@@ -131,11 +131,11 @@
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar: oklch(0.205 0.02 160);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent: oklch(0.269 0.015 160);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);