Changeset 057453c
- Timestamp:
- 02/26/25 10:05:32 (5 weeks ago)
- Branches:
- main
- Children:
- 299af01
- Parents:
- 5d6f37a
- Files:
-
- 21 added
- 2 deleted
- 40 edited
Legend:
- Unmodified
- Added
- Removed
-
package.json
r5d6f37a r057453c 48 48 "lodash": "^4.17.21", 49 49 "mui-one-time-password-input": "^2.0.0", 50 "mvpmasters-shared": "^1.0.9",51 50 "next": "^13.4.19", 52 51 "notistack": "^3.0.1", … … 93 92 "eslint-plugin-unused-imports": "^3.0.0", 94 93 "prettier": "^3.0.3", 95 "prisma": "^6.3.1" 94 "prisma": "^6.3.1", 95 "typescript": "5.7.3" 96 96 }, 97 97 "prisma": { -
prisma/schema.prisma
r5d6f37a r057453c 8 8 } 9 9 10 model C ustomer{10 model Client { 11 11 id String @id @default(uuid()) 12 12 companyId String? // Optional company identifier … … 21 21 status CustomerStatus @default(active) 22 22 23 bankAccounts BankAccount[] // One-to-many relation24 invoicesSent Invoice[] @relation("InvoiceFrom")25 23 invoicesReceived Invoice[] @relation("InvoiceTo") 26 }27 28 model BankAccount {29 id String @id @default(uuid())30 customerId String31 customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)32 accountNumber String?33 bicSwift String?34 iban String?35 routingNumber String?36 currency Currency37 24 } 38 25 … … 40 27 id String @id @default(uuid()) 41 28 name String 42 sprintPrice Float 43 hourPrice Float 44 monthPrice Float 29 sprint Float 30 hour Float 31 month Float 32 tenantId String 33 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) 45 34 46 invoiceItems InvoiceItem[]35 lineItems LineItem[] 47 36 } 48 37 … … 64 53 65 54 invoiceFromId String 66 invoiceFrom Customer @relation("InvoiceFrom",fields: [invoiceFromId], references: [id], onDelete: Cascade)55 invoiceFrom Tenant @relation(fields: [invoiceFromId], references: [id], onDelete: Cascade) 67 56 68 57 invoiceToId String 69 invoiceTo C ustomer@relation("InvoiceTo", fields: [invoiceToId], references: [id], onDelete: Cascade)58 invoiceTo Client @relation("InvoiceTo", fields: [invoiceToId], references: [id], onDelete: Cascade) 70 59 71 items InvoiceItem[]60 items LineItem[] 72 61 } 73 62 74 model InvoiceItem {63 model LineItem { 75 64 id String @id @default(uuid()) 76 65 title String … … 85 74 } 86 75 76 model Tenant { 77 id String @id @default(cuid()) 78 name String 79 email String @unique 80 address Json // Holds {street: string, city?: string, country: string, state?: string, zip: string} 81 bankAccounts Json? // Holds {eur?: {accountNumber?, bicSwift?, iban?, routingNumber?}, usd?: {...}} 82 logoUrl String? 83 phoneNumber String? 84 vatNumber String? 85 companyNumber String? 86 representative String 87 lastInvoiceNumber String @default("0") 88 createdAt DateTime @default(now()) 89 updatedAt DateTime @updatedAt 90 invoicesSent Invoice[] 91 services Service[] 92 employees Employee[] 93 } 94 95 model Employee { 96 id String @id @default(uuid()) 97 name String 98 email String @unique 99 status EmployeeStatus @default(active) 100 iban String? 101 cv String? 102 photo String? 103 project String? 104 tenantId String 105 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) 106 } 107 87 108 // Enums 88 109 enum CustomerStatus { … … 93 114 94 115 enum InvoiceStatus { 95 DRAFT96 PROCESSING97 PENDING98 OVERDUE99 PAID116 draft 117 processing 118 pending 119 overdue 120 paid 100 121 } 101 122 … … 106 127 107 128 enum QuantityType { 108 U NIT109 H OUR110 S PRINT111 M ONTH129 Unit 130 Hour 131 Sprint 132 Month 112 133 } 113 134 114 135 enum Month { 115 J ANUARY116 F EBRUARY117 M ARCH118 A PRIL119 M AY120 J UNE121 J ULY122 A UGUST123 S EPTEMBER124 O CTOBER125 N OVEMBER126 D ECEMBER136 January 137 February 138 March 139 April 140 May 141 June 142 July 143 August 144 September 145 October 146 November 147 December 127 148 } 149 150 enum EmployeeStatus { 151 active 152 inactive 153 } -
prisma/seed.js
r5d6f37a r057453c 4 4 5 5 async function main() { 6 console.log('🌱 Seeding database...'); 6 // Clear existing data 7 await prisma.service.deleteMany(); 8 await prisma.tenant.deleteMany(); 7 9 8 // Create Customers 9 const customer1 = await prisma.customer.create({ 10 data: { 11 name: 'Acme Corp', 12 email: 'contact@acme.com', 13 street: '123 Main St', 14 city: 'New York', 15 country: 'USA', 16 state: 'NY', 17 zip: '10001', 18 phoneNumber: '+1 555-555-5555', 19 vatNumber: 'US123456789', 20 companyNumber: '123456789', 21 representative: 'John Doe', 22 status: 'ACTIVE', 23 logoUrl: 'https://example.com/logo.png', 10 // Define default tenant data 11 const tenantData = { 12 name: "Default Company", 13 email: "contact@defaultcompany.com", 14 address: { 15 street: "123 Business Street", 16 city: "Business City", 17 state: "BS", 18 zip: "12345", 19 country: "United States" 24 20 }, 21 phoneNumber: "+1 234 567 8900", 22 representative: "John Doe", 23 lastInvoiceNumber: "1", 24 logoUrl: "https://example.com/default-logo.png", 25 vatNumber: "VAT123456789", 26 companyNumber: "COMP123456", 27 bankAccounts: { 28 eur: { 29 accountNumber: "1234567890", 30 routingNumber: "987654321", 31 bicSwift: "DEFBANKXXX", 32 iban: "DE89370400440532013000" 33 }, 34 usd: { 35 accountNumber: "0987654321", 36 routingNumber: "123456789", 37 bicSwift: "DEFBANKXXX", 38 iban: "US89370400440532013000" 39 } 40 }, 41 // Add services along with the tenant creation 42 services: { 43 create: [ 44 { 45 name: "Web Development", 46 sprint: 5000, 47 hour: 150, 48 month: 8000 49 }, 50 { 51 name: "UI/UX Design", 52 sprint: 3000, 53 hour: 120, 54 month: 6000 55 }, 56 { 57 name: "Consulting", 58 sprint: 4000, 59 hour: 200, 60 month: 7000 61 } 62 ] 63 } 64 }; 65 66 // Create default tenant with services 67 const defaultTenant = await prisma.tenant.create({ 68 data: tenantData, 69 include: { 70 services: true 71 } 25 72 }); 26 73 27 const customer2 = await prisma.customer.create({ 28 data: { 29 name: 'Globex Ltd.', 30 email: 'info@globex.com', 31 street: '456 Industrial Rd', 32 city: 'Los Angeles', 33 country: 'USA', 34 state: 'CA', 35 zip: '90001', 36 phoneNumber: '+1 555-123-4567', 37 vatNumber: 'US987654321', 38 companyNumber: '987654321', 39 representative: 'Jane Smith', 40 status: 'INACTIVE', 41 logoUrl: 'https://example.com/logo2.png', 42 }, 43 }); 74 console.log('Seeded default tenant:', defaultTenant); 44 75 45 // Create Bank Accounts 46 await prisma.bankAccount.createMany({ 47 data: [ 48 { 49 customerId: customer1.id, 50 accountNumber: '1234567890', 51 bicSwift: 'ACMEUS33', 52 iban: 'US12345678901234567890', 53 routingNumber: '111000025', 54 currency: 'USD', 55 }, 56 { 57 customerId: customer2.id, 58 accountNumber: '0987654321', 59 bicSwift: 'GLOBEXUS12', 60 iban: 'US09876543210987654321', 61 routingNumber: '222000033', 62 currency: 'EUR', 63 }, 64 ], 65 }); 66 67 // Create Services 68 const service1 = await prisma.service.create({ 69 data: { 70 name: 'Web Development', 71 sprintPrice: 5000.0, 72 hourPrice: 100.0, 73 monthPrice: 20000.0, 74 }, 75 }); 76 77 const service2 = await prisma.service.create({ 78 data: { 79 name: 'SEO Optimization', 80 sprintPrice: 3000.0, 81 hourPrice: 75.0, 82 monthPrice: 12000.0, 83 }, 84 }); 85 86 // Create Invoices 87 const invoice1 = await prisma.invoice.create({ 88 data: { 89 dueDate: new Date('2025-03-15'), 90 status: 'PENDING', 91 currency: 'USD', 92 quantityType: 'HOUR', 93 subTotal: 5000.0, 94 createDate: new Date(), 95 month: 'FEBRUARY', 96 discount: 0.0, 97 taxes: 500.0, 98 totalAmount: 5500.0, 99 invoiceNumber: 'INV-2025-001', 100 pdfRef: 'https://example.com/invoice1.pdf', 101 invoiceFromId: customer1.id, 102 invoiceToId: customer2.id, 103 }, 104 }); 105 106 // Create Invoice Items 107 await prisma.invoiceItem.create({ 108 data: { 109 title: 'Web Development - Sprint 1', 110 price: 5000.0, 111 total: 5000.0, 112 quantity: 1, 113 description: 'Development of the MVP frontend', 114 serviceId: service1.id, 115 invoiceId: invoice1.id, 116 }, 117 }); 76 console.log('🌱 Seeding database...'); 118 77 119 78 console.log('✅ Seeding complete!'); … … 122 81 main() 123 82 .catch((e) => { 124 console.error( e);83 console.error('Error seeding database:', e); 125 84 process.exit(1); 126 85 }) -
src/api/customer.ts
r5d6f37a r057453c 1 1 import { useMemo } from 'react'; 2 2 // types 3 import { Customer } from ' mvpmasters-shared';3 import { Customer } from 'src/schemas'; 4 4 // db 5 5 import { endpoints, fetcher } from 'src/utils/axios'; -
src/api/invoice.ts
r5d6f37a r057453c 1 1 import { useMemo } from 'react'; 2 2 // types 3 import { Invoice } from 'mvpmasters-shared';3 import { Invoice, UpdateInvoice } from 'src/schemas'; 4 4 // db 5 5 import useSWR from 'swr'; 6 6 import { endpoints, fetcher } from 'src/utils/axios'; 7 import axios from 'src/utils/axios'; 8 import { mutate } from 'swr'; 9 import { useSWRConfig } from 'swr'; 7 10 8 11 interface InvoiceFilters { … … 51 54 } 52 55 53 //export function useGetInvoice({ id }: { id: string }) {54 // const collectionName = collections.invoice;56 export function useGetInvoice({ id }: { id: string }) { 57 const path = endpoints.invoice; 55 58 56 //const { data, isLoading, error, isValidating } = useSWR(57 // [collectionName, id],58 // () => documentFetcher<Invoice>(collectionName, id),59 //{60 //revalidateOnFocus: false,61 //}62 //);59 const { data, isLoading, error, isValidating } = useSWR( 60 `${path}/${id}`, 61 () => fetcher<Invoice>(`${path}/${id}`), 62 { 63 revalidateOnFocus: false, 64 } 65 ); 63 66 64 //const memoizedValue = useMemo(65 //() => ({66 //currentInvoice: data || null,67 //currentInvoiceLoading: isLoading,68 //currentInvoiceError: error,69 //currentInvoiceValidating: isValidating,70 //currentInvoiceEmpty: !isLoading && !data,71 //}),72 //[data, error, isLoading, isValidating]73 //);67 const memoizedValue = useMemo( 68 () => ({ 69 currentInvoice: data || null, 70 currentInvoiceLoading: isLoading, 71 currentInvoiceError: error, 72 currentInvoiceValidating: isValidating, 73 currentInvoiceEmpty: !isLoading && !data, 74 }), 75 [data, error, isLoading, isValidating] 76 ); 74 77 75 // return memoizedValue; 76 // } 78 return memoizedValue; 79 } 80 81 // Add this interface for the create invoice payload 82 interface CreateInvoicePayload { 83 createDate: Date; 84 dueDate: Date; 85 items: any[]; 86 invoiceNumber: string; 87 invoiceFrom: any; 88 invoiceTo: any; 89 currency: string; 90 quantityType: string; 91 month: string; 92 status?: string; 93 taxes?: number; 94 discount?: number; 95 totalAmount: number; 96 pdfRef?: string; 97 } 98 99 export async function createInvoice(data: CreateInvoicePayload): Promise<Invoice> { 100 const response = await axios.post<Invoice>(endpoints.invoice, data); 101 102 // Mutate the SWR cache to include the new invoice 103 await mutate( 104 endpoints.invoice, 105 (existingInvoices: Invoice[] = []) => [response.data, ...existingInvoices], 106 false // Set to false to avoid revalidation since we already have the new data 107 ); 108 109 return response.data; 110 } 111 112 export async function updateInvoice(id: string, data: Partial<UpdateInvoice>) { 113 const response = await axios.patch<Invoice>(`${endpoints.invoice}/${id}`, data); 114 115 // Mutate the individual invoice cache 116 await mutate(`${endpoints.invoice}/${id}`, response.data, false); 117 118 // Mutate the invoice list cache 119 await mutate( 120 endpoints.invoice, 121 (existingInvoices: Invoice[] = []) => 122 existingInvoices.map((invoice) => (invoice.id === id ? response.data : invoice)), 123 false 124 ); 125 126 return response.data; 127 } 128 129 export async function deleteInvoice(id: string) { 130 const response = await axios.delete<Invoice>(`${endpoints.invoice}/${id}`); 131 132 // Mutate the invoice list cache to remove the deleted invoice 133 await mutate( 134 endpoints.invoice, 135 (existingInvoices: Invoice[] = []) => existingInvoices.filter((invoice) => invoice.id !== id), 136 false 137 ); 138 139 return response.data; 140 } 141 142 // Update the useDeleteInvoice hook to use the new implementation 143 export function useDeleteInvoice() { 144 const deleteInvoiceMutation = async (id: string) => { 145 try { 146 await deleteInvoice(id); 147 return true; 148 } catch (error) { 149 console.error('Error deleting invoice:', error); 150 throw error; 151 } 152 }; 153 154 return { deleteInvoiceMutation }; 155 } -
src/api/service.ts
r5d6f37a r057453c 1 1 import { useMemo } from 'react'; 2 2 // types 3 import { Service } from 'mvpmasters-shared'; 4 // db 5 import { collections, collectionFetcher as fetcher } from 'src/lib/firestore'; 3 import { Service } from 'src/schemas'; 6 4 // swr 7 5 import useSWR from 'swr'; 6 import { endpoints, fetcher } from 'src/utils/axios'; 8 7 9 8 export function useGetServices() { 10 const collectionName = collections.service; 11 12 const { data, isLoading, error, isValidating } = useSWR(collectionName, fetcher<Service>, { 13 revalidateOnFocus: false, 14 }); 9 const { data, isLoading, error, isValidating } = useSWR<Service[]>( 10 endpoints.service, 11 () => fetcher<Service[]>(endpoints.service), 12 { 13 revalidateOnFocus: false, 14 } 15 ); 15 16 16 17 const memoizedValue = useMemo( -
src/app/api/customers/[id]/route.ts
r5d6f37a r057453c 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 import { customerSchema } from ' mvpmasters-shared';2 import { customerSchema } from 'src/schemas'; 3 3 import prisma from 'src/lib/prisma'; 4 4 import { authenticateRequest } from 'src/lib/auth-middleware'; … … 14 14 const validatedData = customerSchema.partial().parse(body); 15 15 16 const customer = await prisma.customer.update({ 17 where: { id: params.id, userId }, 18 data: validatedData, 16 const customer = await prisma.client.update({ 17 where: { id: params.id }, 18 data: { 19 ...validatedData, 20 bankAccounts: undefined, 21 }, 19 22 }); 20 23 -
src/app/api/customers/route.ts
r5d6f37a r057453c 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 import { customerTableFiltersSchema, newCustomerSchema } from ' mvpmasters-shared';2 import { customerTableFiltersSchema, newCustomerSchema } from 'src/schemas'; 3 3 import prisma from 'src/lib/prisma'; 4 4 import { authenticateRequest } from 'src/lib/auth-middleware'; 5 import { CustomerStatus } from '@prisma/client'; 5 6 6 7 export async function GET(request: NextRequest) { … … 23 24 const validatedFilters = customerTableFiltersSchema.parse(filters); 24 25 25 const customers = await prisma.c ustomer.findMany({26 const customers = await prisma.client.findMany({ 26 27 where: { 27 28 name: { contains: validatedFilters.name, mode: 'insensitive' }, 28 status: validatedFilters.status ? { equals: validatedFilters.status } : undefined, 29 status: validatedFilters.status 30 ? { equals: validatedFilters.status as CustomerStatus } 31 : undefined, 29 32 }, 30 33 }); … … 50 53 console.log('validatedData', validatedData); 51 54 52 const customer = await prisma.c ustomer.create({55 const customer = await prisma.client.create({ 53 56 data: { 54 57 ...validatedData, -
src/app/api/invoices/[id]/route.ts
r5d6f37a r057453c 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 import { invoiceSchema } from 'mvpmasters-shared';2 import { invoiceSchema, updateInvoiceSchema } from 'src/schemas'; 3 3 import prisma from 'src/lib/prisma'; 4 4 import { authenticateRequest } from 'src/lib/auth-middleware'; 5 import { Prisma } from '@prisma/client'; 5 6 6 export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {7 export async function GET(request: NextRequest, { params }: { params: { id: string } }) { 7 8 try { 8 // Authenticate the request 9 // Validate ID format 10 if (!params.id || !/^[0-9a-fA-F-]+$/.test(params.id)) { 11 return NextResponse.json({ error: 'Invalid invoice ID format' }, { status: 400 }); 12 } 13 14 // Authenticate request 9 15 const authResult = await authenticateRequest(request); 10 16 if (authResult instanceof NextResponse) { … … 13 19 const { userId } = authResult; 14 20 15 const body = await request.json(); 16 const validatedData = invoiceSchema.partial().parse(body); 17 18 const invoice = await prisma.invoice.update({ 19 where: { id: params.id, userId }, 20 data: { 21 ...validatedData, 22 items: validatedData.items 23 ? { 24 deleteMany: {}, 25 create: validatedData.items, 26 } 27 : undefined, 21 // Fetch invoice with user check 22 const invoice = await prisma.invoice.findFirst({ 23 where: { 24 id: params.id, 25 // invoiceFromId: userId, 28 26 }, 29 27 include: { 30 items: {31 include: {32 service: true,33 },34 },35 28 invoiceFrom: true, 36 29 invoiceTo: true, 30 items: true, 37 31 }, 38 32 }); 39 33 34 if (!invoice) { 35 return NextResponse.json({ error: 'Invoice not found or access denied' }, { status: 404 }); 36 } 37 40 38 return NextResponse.json(invoice); 41 39 } catch (error) { 42 return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 40 console.error('Error fetching invoice:', error); 41 42 if (error instanceof Prisma.PrismaClientKnownRequestError) { 43 return NextResponse.json({ error: 'Database error occurred' }, { status: 500 }); 44 } 45 46 return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); 43 47 } 44 48 } 49 50 export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { 51 try { 52 // Validate ID format 53 if (!params.id || !/^[0-9a-fA-F-]+$/.test(params.id)) { 54 return NextResponse.json({ error: 'Invalid invoice ID format' }, { status: 400 }); 55 } 56 57 // Authenticate request 58 const authResult = await authenticateRequest(request); 59 if (authResult instanceof NextResponse) { 60 return authResult; 61 } 62 const { userId } = authResult; 63 64 // Parse and validate request body 65 const body = await request.json(); 66 67 const validation = updateInvoiceSchema.partial().safeParse(body); 68 69 if (!validation.success) { 70 return NextResponse.json( 71 { error: 'Invalid invoice data', details: validation.error.format() }, 72 { status: 400 } 73 ); 74 } 75 76 // Verify invoice exists and belongs to user 77 const existingInvoice = await prisma.invoice.findFirst({ 78 where: { 79 id: params.id, 80 // invoiceFromId: userId, 81 }, 82 }); 83 84 if (!existingInvoice) { 85 return NextResponse.json({ error: 'Invoice not found or access denied' }, { status: 404 }); 86 } 87 88 // Update invoice and related data 89 const updatedInvoice = await prisma.$transaction(async (tx) => { 90 // Conditionally delete and recreate items only if they are provided 91 if (validation.data.items) { 92 await tx.invoiceItem.deleteMany({ 93 where: { invoiceId: params.id }, 94 }); 95 } 96 97 // Update the invoice and create new items if provided 98 return tx.invoice.update({ 99 where: { id: params.id }, 100 data: { 101 invoiceNumber: validation.data.invoiceNumber, 102 createDate: validation.data.createDate, 103 dueDate: validation.data.dueDate, 104 status: validation.data.status, 105 currency: validation.data.currency, 106 quantityType: validation.data.quantityType, 107 subTotal: validation.data.subTotal, 108 month: validation.data.month, 109 totalAmount: validation.data.totalAmount, 110 discount: validation.data.discount, 111 taxes: validation.data.taxes, 112 pdfRef: validation.data.pdfRef, 113 invoiceTo: { 114 update: validation.data.invoiceTo, 115 }, 116 items: validation.data.items 117 ? { 118 create: validation.data.items.map((item) => ({ 119 ...item, 120 service: { 121 connect: { id: item.service.id }, 122 }, 123 })), 124 } 125 : undefined, 126 }, 127 include: { 128 invoiceFrom: true, 129 invoiceTo: true, 130 items: true, 131 }, 132 }); 133 }); 134 135 return NextResponse.json(updatedInvoice); 136 } catch (error) { 137 console.error('Error updating invoice:', error); 138 139 if (error instanceof Prisma.PrismaClientKnownRequestError) { 140 return NextResponse.json({ error: 'Database error occurred' }, { status: 500 }); 141 } 142 143 return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); 144 } 145 } 146 147 export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { 148 try { 149 // Validate ID format 150 if (!params.id || !/^[0-9a-fA-F-]+$/.test(params.id)) { 151 return NextResponse.json({ error: 'Invalid invoice ID format' }, { status: 400 }); 152 } 153 154 // Authenticate request 155 const authResult = await authenticateRequest(request); 156 if (authResult instanceof NextResponse) { 157 return authResult; 158 } 159 const { userId } = authResult; 160 console.log('userId', userId); 161 162 // Verify invoice exists and belongs to user 163 const existingInvoice = await prisma.invoice.findFirst({ 164 where: { 165 id: params.id, 166 }, 167 }); 168 169 if (!existingInvoice) { 170 return NextResponse.json({ error: 'Invoice not found or access denied' }, { status: 404 }); 171 } 172 173 // Delete invoice and related items in a transaction 174 await prisma.$transaction(async (tx) => { 175 // Delete related items first 176 await tx.invoiceItem.deleteMany({ 177 where: { invoiceId: params.id }, 178 }); 179 180 // Delete the invoice 181 await tx.invoice.delete({ 182 where: { id: params.id }, 183 }); 184 }); 185 186 return NextResponse.json({ message: 'Invoice deleted successfully' }, { status: 200 }); 187 } catch (error) { 188 console.error('Error deleting invoice:', error); 189 190 if (error instanceof Prisma.PrismaClientKnownRequestError) { 191 if (error.code === 'P2025') { 192 return NextResponse.json({ error: 'Invoice not found' }, { status: 404 }); 193 } 194 return NextResponse.json({ error: 'Database error occurred' }, { status: 500 }); 195 } 196 197 return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); 198 } 199 } -
src/app/api/invoices/route.ts
r5d6f37a r057453c 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 import { invoiceSchema, invoiceTableFiltersSchema } from 'mvpmasters-shared';2 import { createInvoiceSchema, invoiceSchema, invoiceTableFiltersSchema } from 'src/schemas'; 3 3 import prisma from 'src/lib/prisma'; 4 4 import { auth } from 'src/lib/firebase-admin'; 5 import { InvoiceStatus } from '@prisma/client'; 5 6 6 7 // Helper function to get userId from Authorization header … … 41 42 const invoices = await prisma.invoice.findMany({ 42 43 where: { 43 userId, 44 status: validatedFilters.status ? { equals: validatedFilters.status } : undefined, 44 status: validatedFilters.status 45 ? { equals: validatedFilters.status as InvoiceStatus } 46 : undefined, 45 47 createDate: { 46 gte: validatedFilters.startDate,47 lte: validatedFilters.endDate,48 ...(validatedFilters.startDate && { gte: validatedFilters.startDate }), 49 ...(validatedFilters.endDate && { lte: validatedFilters.endDate }), 48 50 }, 49 51 items: … … 77 79 78 80 const body = await request.json(); 79 const validatedData = invoiceSchema.parse(body); 81 const validatedData = createInvoiceSchema.parse(body); 82 83 const tenant = await prisma.tenant.findUnique({ 84 where: { id: validatedData.invoiceFrom.id }, 85 }); 86 87 const toCustomer = await prisma.client.findUnique({ 88 where: { id: validatedData.invoiceTo.id }, 89 }); 90 91 if (!tenant || !toCustomer) { 92 return NextResponse.json({ error: 'Invoice sender or recipient not found' }, { status: 404 }); 93 } 94 95 // Update lastInvoiceNumber in tenant 96 const updatedTenant = await prisma.tenant.update({ 97 where: { id: tenant.id }, 98 data: { 99 lastInvoiceNumber: validatedData.invoiceNumber, 100 }, 101 }); 80 102 81 103 const invoice = await prisma.invoice.create({ 82 104 data: { 83 ...validatedData, 84 userId, 105 dueDate: validatedData.dueDate, 106 status: validatedData.status, 107 currency: validatedData.currency, 108 quantityType: validatedData.quantityType, 109 subTotal: validatedData.subTotal, 110 createDate: validatedData.createDate, 111 month: validatedData.month, 112 discount: validatedData.discount, 113 taxes: validatedData.taxes, 114 totalAmount: validatedData.totalAmount, 115 invoiceNumber: validatedData.invoiceNumber, 116 invoiceFromId: tenant.id, 117 invoiceToId: toCustomer.id, 85 118 items: { 86 create: validatedData.items, 119 create: validatedData.items.map((item) => ({ 120 title: item.title, 121 price: item.price, 122 total: item.total, 123 quantity: item.quantity, 124 description: item.description, 125 service: { 126 connect: { id: item.service.id }, 127 }, 128 })), 87 129 }, 88 130 }, … … 100 142 return NextResponse.json(invoice, { status: 201 }); 101 143 } catch (error) { 144 console.error(error); 102 145 return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 103 146 } -
src/layouts/dashboard/config-navigation.tsx
r5d6f37a r057453c 53 53 items: [ 54 54 { 55 title: ' banking',55 title: 'dashboard', 56 56 path: paths.dashboard.banking, 57 57 icon: ICONS.banking, … … 85 85 ], 86 86 }, 87 { 88 title: 'Employees', 89 path: paths.dashboard.employee.list, 90 icon: <SvgColor src="/assets/icons/navbar/ic_user.svg" />, 91 children: [ 92 { title: 'list', path: paths.dashboard.employee.list }, 93 { title: 'create', path: paths.dashboard.employee.new }, 94 ], 95 }, 87 96 ], 88 97 }, -
src/lib/auth-middleware.ts
r5d6f37a r057453c 4 4 export interface AuthenticatedRequest extends NextRequest { 5 5 userId: string; 6 tenantId: string; 6 7 } 7 8 8 9 export async function authenticateRequest( 9 10 request: NextRequest 10 ): Promise<{ userId: string } | NextResponse> {11 ): Promise<{ userId: string; tenantId: string } | NextResponse> { 11 12 // Get the authorization header 12 13 const authHeader = request.headers.get('Authorization'); … … 22 23 const decodedToken = await auth.verifyIdToken(token); 23 24 const userId = decodedToken.uid; 25 const tenantId = decodedToken.customClaims?.tenantId || 'cm7bwtjy80000pb0m5qenk8am'; 24 26 25 if (!userId ) {27 if (!userId || !tenantId) { 26 28 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 27 29 } 28 30 29 return { userId };31 return { userId, tenantId }; 30 32 } catch (error) { 31 33 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); -
src/routes/paths.ts
r5d6f37a r057453c 34 34 list: `${ROOTS.DASHBOARD}/customer/list`, 35 35 }, 36 employee: { 37 root: '/dashboard/employee', 38 list: '/dashboard/employee/list', 39 new: '/dashboard/employee/new', 40 edit: (id: string) => `/dashboard/employee/${id}/edit`, 41 }, 36 42 }, 37 43 }; -
src/sections/company/company-item.tsx
r5d6f37a r057453c 4 4 import Stack, { StackProps } from '@mui/material/Stack'; 5 5 // types 6 import { Customer } from ' mvpmasters-shared';6 import { Customer } from 'src/schemas'; 7 7 // components 8 8 import Label from 'src/components/label'; -
src/sections/company/company-list-dialog.tsx
r5d6f37a r057453c 9 9 import ListItemButton, { listItemButtonClasses } from '@mui/material/ListItemButton'; 10 10 // types 11 import { Customer } from ' mvpmasters-shared';11 import { Customer } from 'src/schemas'; 12 12 // components 13 13 import Iconify from 'src/components/iconify'; 14 14 import SearchNotFound from 'src/components/search-not-found'; 15 15 import { createFullAddress } from 'src/utils/create-full-address'; 16 import { Tenant } from 'src/schemas'; 16 17 17 18 // ---------------------------------------------------------------------- … … 19 20 type Props = { 20 21 title?: string; 21 list: Customer[]; 22 tenant?: Tenant; 23 list?: Customer[]; 22 24 action?: React.ReactNode; 23 25 // … … 26 28 // 27 29 selected: (selectedId: string) => boolean; 28 onSelect: (address: Customer | null) => void;30 onSelect: (address: Customer | Tenant | null) => void; 29 31 }; 30 32 31 33 export default function AddressListDialog({ 32 34 title = 'Address Book', 35 tenant, 33 36 list, 34 37 action, … … 43 46 44 47 const dataFiltered = applyFilter({ 45 inputData: list ,48 inputData: list || (tenant ? [tenant] : []), 46 49 query: searchCompany, 47 50 }); … … 54 57 55 58 const handleSelectCompany = useCallback( 56 (company: Customer | null) => {59 (company: Customer | Tenant | null) => { 57 60 onSelect(company); 58 61 setSearchCompany(''); … … 156 159 // ---------------------------------------------------------------------- 157 160 158 function applyFilter({ inputData, query }: { inputData: Customer[]; query: string }) {161 function applyFilter({ inputData, query }: { inputData: Tenant[] | Customer[]; query: string }) { 159 162 if (query) { 160 163 return inputData.filter( -
src/sections/invoice/invoice-details.tsx
r5d6f37a r057453c 18 18 import { fCurrency } from 'src/utils/format-number'; 19 19 // types 20 import { Invoice } from ' mvpmasters-shared';20 import { Invoice } from 'src/schemas'; 21 21 // components 22 22 import Label from 'src/components/label'; … … 28 28 import { getQuantityType } from 'src/utils/get-invoice-quantity-type'; 29 29 import InvoiceToolbar from './invoice-toolbar'; 30 import { updateInvoice } from 'src/api/invoice'; 30 31 31 32 // ---------------------------------------------------------------------- … … 59 60 const handleChangeStatus = useCallback( 60 61 async (event: React.ChangeEvent<HTMLInputElement>) => { 61 await updateDocument(collections.invoice, invoice.id, { status: event.target.value }); 62 // await updateDocument(collections.invoice, invoice.id, { status: event.target.value }); 63 await updateInvoice(invoice.id, { 64 status: event.target.value as 'draft' | 'processing' | 'pending' | 'overdue' | 'paid', 65 }); 62 66 mutate([collections.invoice, invoice.id]); 63 67 }, … … 240 244 {invoice.invoiceTo.name} 241 245 <br /> 242 {!!invoice.invoiceTo.company Id&& (246 {!!invoice.invoiceTo.companyNumber && ( 243 247 <> 244 Company ID: {invoice.invoiceTo.company Id}248 Company ID: {invoice.invoiceTo.companyNumber} 245 249 <br /> 246 250 </> … … 257 261 Date Issued 258 262 </Typography> 259 {fDate(invoice.createDate .toDate())}263 {fDate(invoice.createDate)} 260 264 </Stack> 261 265 … … 264 268 Due Date 265 269 </Typography> 266 {fDate(invoice.dueDate .toDate())}270 {fDate(invoice.dueDate)} 267 271 </Stack> 268 272 </Box> -
src/sections/invoice/invoice-ee-pdf.tsx
r5d6f37a r057453c 5 5 import { fCurrency } from 'src/utils/format-number'; 6 6 // types 7 import { Invoice } from ' mvpmasters-shared';7 import { Invoice } from 'src/schemas'; 8 8 import { createFullAddress } from 'src/utils/create-full-address'; 9 9 import { getQuantityType } from 'src/utils/get-invoice-quantity-type'; … … 229 229 <View style={styles.col6}> 230 230 <Text style={[styles.subtitle1, styles.mb4]}>Date Issued</Text> 231 <Text style={styles.body2}>{fDate(createDate .toDate())}</Text>231 <Text style={styles.body2}>{fDate(createDate)}</Text> 232 232 </View> 233 233 <View style={styles.col6}> 234 234 <Text style={[styles.subtitle1, styles.mb4]}>Due date</Text> 235 <Text style={styles.body2}>{fDate(dueDate .toDate())}</Text>235 <Text style={styles.body2}>{fDate(dueDate)}</Text> 236 236 </View> 237 237 </View> -
src/sections/invoice/invoice-mk-pdf.tsx
r5d6f37a r057453c 5 5 import { fCurrency } from 'src/utils/format-number'; 6 6 // types 7 import { Invoice } from ' mvpmasters-shared';7 import { Invoice } from 'src/schemas'; 8 8 import { createFullAddress } from 'src/utils/create-full-address'; 9 9 import { getQuantityType } from 'src/utils/get-invoice-quantity-type'; … … 179 179 <View style={styles.col6}> 180 180 <Text style={[styles.subtitle1, styles.mb4]}>Date Issued</Text> 181 <Text style={styles.body2}>{fDate(createDate .toDate())}</Text>181 <Text style={styles.body2}>{fDate(createDate)}</Text> 182 182 </View> 183 183 <View style={styles.col6}> 184 184 <Text style={[styles.subtitle1, styles.mb4]}>Due date</Text> 185 <Text style={styles.body2}>{fDate(dueDate .toDate())}</Text>185 <Text style={styles.body2}>{fDate(dueDate)}</Text> 186 186 </View> 187 187 </View> -
src/sections/invoice/invoice-new-edit-address.tsx
r5d6f37a r057453c 7 7 import Typography from '@mui/material/Typography'; 8 8 // hooks 9 import { useGet Settings } from 'src/api/settings';9 import { useGetTenant } from 'src/api/tenant'; 10 10 import { useBoolean } from 'src/hooks/use-boolean'; 11 11 import { useResponsive } from 'src/hooks/use-responsive'; … … 32 32 const { invoiceFrom, invoiceTo } = values; 33 33 34 const { settings } = useGetSettings();34 const { settings: tenant } = useGetTenant(); 35 35 const { customers } = useGetCustomers(); 36 36 … … 100 100 </Stack> 101 101 102 { settings&& (102 {tenant && ( 103 103 <CompanyListDialog 104 104 title="Companies" … … 106 106 onClose={from.onFalse} 107 107 selected={(selectedId: string) => invoiceFrom?.id === selectedId} 108 onSelect={( company) => setValue('invoiceFrom', company)}109 list={[settings?.company, settings?.['company-ee']]}108 onSelect={(tenant) => setValue('invoiceFrom', tenant)} 109 tenant={tenant} 110 110 action={ 111 111 <Button -
src/sections/invoice/invoice-new-edit-details.tsx
r5d6f37a r057453c 14 14 import { fCurrency } from 'src/utils/format-number'; 15 15 // types 16 import { InvoiceItem } from ' mvpmasters-shared';16 import { InvoiceItem } from 'src/schemas'; 17 17 // components 18 18 import Iconify from 'src/components/iconify'; … … 75 75 const handleSelectService = useCallback( 76 76 (index: number, option: string) => { 77 setValue( 78 `items[${index}].price`, 79 invoiceServices.find((service) => service.id === option)?.price?.[ 80 values.quantityType?.toLowerCase() as 'sprint' | 'hour' | 'month' 81 ] 82 ); 83 setValue( 84 `items[${index}].total`, 85 values.items.map((item: InvoiceItem) => item.quantity * item.price)[index] 86 ); 77 const service = invoiceServices.find((service) => service.id === option); 78 if (!service) return; 79 80 const quantityType = values.quantityType?.toLowerCase(); 81 let price = 0; 82 83 switch (quantityType) { 84 case 'sprint': 85 price = service.sprint; 86 break; 87 case 'hour': 88 price = service.hour; 89 break; 90 case 'month': 91 price = service.month; 92 break; 93 default: 94 price = 0; 95 } 96 97 setValue(`items[${index}].price`, price); 98 setValue(`items[${index}].total`, values.items[index].quantity * price); 87 99 }, 88 100 [setValue, values.items, values.quantityType, invoiceServices] -
src/sections/invoice/invoice-new-edit-form.tsx
r5d6f37a r057453c 11 11 import { useRouter } from 'src/routes/hooks'; 12 12 // types 13 import { Invoice } from 'mvpmasters-shared';13 import { CreateInvoice, Invoice } from 'src/schemas'; 14 14 // hooks 15 15 import { useBoolean } from 'src/hooks/use-boolean'; … … 20 20 import uploadToFirebaseStorage from 'src/utils/upload-to-firebase-storage'; 21 21 import { pdf } from '@react-pdf/renderer'; 22 import { useGetSettings } from 'src/api/settings'; 23 import { 24 collections, 25 documents, 26 firestoreBatch, 27 generateId, 28 updateDocument, 29 } from 'src/lib/firestore'; 22 import { useGetTenant } from 'src/api/tenant'; 23 import { collections, generateId, updateDocument } from 'src/lib/firestore'; 30 24 import { useGetServices } from 'src/api/service'; 31 25 import { mutate } from 'swr'; 32 import { Timestamp } from 'firebase/firestore';33 26 import InvoiceNewEditStatusDate from './invoice-new-edit-status-date'; 34 27 import InvoiceNewEditAddress from './invoice-new-edit-address'; 35 28 import InvoiceNewEditDetails from './invoice-new-edit-details'; 36 29 import InvoicePDF from './invoice-pdf'; 30 import { createInvoice, updateInvoice } from 'src/api/invoice'; 37 31 38 32 // ---------------------------------------------------------------------- … … 58 52 ]; 59 53 54 interface InvoiceItem { 55 service: string | null; 56 title: string; 57 price: number; 58 total: number; 59 quantity: number; 60 description: string; 61 } 62 60 63 export default function InvoiceNewEditForm({ isCopy, currentInvoice }: Props) { 61 64 const router = useRouter(); … … 69 72 description: Yup.string().required('Description is required'), 70 73 service: Yup.string().nullable(), 71 quantity: Yup.number().required('Quantity is required').min(0.5, 'Quantity must be at least 1'), 74 quantity: Yup.number() 75 .required('Quantity is required') 76 .min(0.5, 'Quantity must be at least 0.5'), 72 77 price: Yup.number().required('Price is required').min(0, 'Price must be at least 0'), 73 78 total: Yup.number().required('Total is required').min(0, 'Total must be at least 0'), … … 91 96 .required('Quantity type is required'), 92 97 month: Yup.string().oneOf(monthNames).required('Month is required'), 93 status: Yup.string(). required(),98 status: Yup.string().oneOf(['draft', 'processing', 'pending', 'overdue', 'paid']).required(), 94 99 totalAmount: Yup.number().required(), 95 100 // not required … … 103 108 104 109 const { services: invoiceServices } = useGetServices(); 105 const { settings, settingsEmpty, settingsLoading } = useGetSettings(); 110 console.log('invoiceServices', invoiceServices); 111 const { settings: tenant, settingsEmpty, settingsLoading } = useGetTenant(); 106 112 107 113 const defaultValues = useMemo( … … 110 116 !isCopy && currentInvoice?.invoiceNumber 111 117 ? currentInvoice?.invoiceNumber 112 : incrementInvoiceNumber(settings?.invoice?.lastInvoiceNumber), 113 createDate: currentInvoice?.createDate?.toDate() || new Date(), 114 dueDate: currentInvoice?.dueDate?.toDate() || null, 118 : incrementInvoiceNumber(tenant?.lastInvoiceNumber), 119 createDate: currentInvoice?.createDate ? new Date(currentInvoice.createDate) : new Date(), 120 dueDate: currentInvoice?.dueDate 121 ? new Date(currentInvoice.dueDate) 122 : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 115 123 invoiceFrom: currentInvoice?.invoiceFrom || null, 116 124 invoiceTo: currentInvoice?.invoiceTo || null, … … 138 146 totalAmount: currentInvoice?.totalAmount || 0, 139 147 }), 140 [currentInvoice, isCopy, settings]148 [currentInvoice, isCopy, tenant] 141 149 ); 142 150 … … 164 172 165 173 try { 166 // generate collection id167 174 const id = generateId(collections.invoice); 175 176 // Ensure dates are valid Date objects 177 const createDate = 178 data.createDate instanceof Date ? data.createDate : new Date(data.createDate); 179 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); 180 181 const currentTime = new Date(); 182 createDate.setHours( 183 currentTime.getHours(), 184 currentTime.getMinutes(), 185 currentTime.getSeconds() 186 ); 168 187 169 188 // attach serivce details … … 173 192 })); 174 193 175 const currentTime = new Date();176 const createDateWithCurrentTime = new Date(data.createDate); // This creates a date object using the date from data.createDate177 // Set the time of createDateWithCurrentTime to the current hour, minutes, and seconds178 createDateWithCurrentTime.setHours(179 currentTime.getHours(),180 currentTime.getMinutes(),181 currentTime.getSeconds()182 );183 184 194 // transform data 185 const writeData = {195 const writeData: CreateInvoice = { 186 196 ...data, 187 invoiceNumber: incrementInvoiceNumber( settings?.invoice.lastInvoiceNumber),197 invoiceNumber: incrementInvoiceNumber(tenant?.lastInvoiceNumber), 188 198 status: 'draft', 189 createDate: Timestamp.fromDate(createDateWithCurrentTime), 190 dueDate: Timestamp.fromDate(new Date(data.dueDate)), 191 items, 199 createDate, 200 dueDate, 201 items: items.filter((item) => item.service !== null) as CreateInvoice['items'], 202 subTotal: data.totalAmount - (data.taxes || 0) - (data.discount || 0), 203 month: data.month as Invoice['month'], 204 invoiceFrom: data.invoiceFrom!, 205 invoiceTo: data.invoiceTo!, 192 206 }; 193 207 … … 199 213 200 214 // write to DB 201 await firestoreBatch([ 202 { 203 docPath: `${collections.invoice}/${id}`, 204 type: 'set', 205 data: { 206 ...writeData, 207 pdfRef: storagePath, 208 }, 209 }, 210 { 211 docPath: `${collections.settings}/${documents.settingsInvoice}`, 212 type: 'set', 213 data: { lastInvoiceNumber: writeData.invoiceNumber }, 214 }, 215 ]); 215 // await firestoreBatch([ 216 // { 217 // docPath: `${collections.invoice}/${id}`, 218 // type: 'set', 219 // data: { 220 // ...writeData, 221 // pdfRef: storagePath, 222 // }, 223 // }, 224 // { 225 // docPath: `${collections.settings}/${documents.settingsInvoice}`, 226 // type: 'set', 227 // data: { lastInvoiceNumber: writeData.invoiceNumber }, 228 // }, 229 // ]); 230 231 await createInvoice({ ...writeData, pdfRef: storagePath }); 216 232 217 233 loadingSave.onFalse(); … … 229 245 try { 230 246 if (currentInvoice) { 247 // Ensure dates are valid Date objects 248 const createDate = 249 data.createDate instanceof Date ? data.createDate : new Date(data.createDate); 250 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); 251 231 252 // attach serivce details 232 253 const items = data.items.map((item) => ({ … … 238 259 const writeData = { 239 260 ...data, 240 createDate : Timestamp.fromDate(new Date(data.createDate)),241 dueDate : Timestamp.fromDate(new Date(data.dueDate)),261 createDate, 262 dueDate, 242 263 items, 264 invoiceFrom: data.invoiceFrom!, 265 invoiceTo: data.invoiceTo!, 243 266 }; 244 267 … … 250 273 251 274 // update DB 252 await updateDocument(collections.invoice, currentInvoice.id, { 275 // await updateDocument(collections.invoice, currentInvoice.id, { 276 // ...writeData, 277 // pdfRef: storagePath, 278 // }); 279 280 await updateInvoice(currentInvoice.id, { 253 281 ...writeData, 254 282 pdfRef: storagePath, 283 status: data.status as 'draft' | 'processing' | 'pending' | 'overdue' | 'paid', 284 month: data.month as 285 | 'January' 286 | 'February' 287 | 'March' 288 | 'April' 289 | 'May' 290 | 'June' 291 | 'July' 292 | 'August' 293 | 'September' 294 | 'October' 295 | 'November' 296 | 'December', 297 items: items.filter((item) => item.service !== null) as { 298 service: { id: string; month: number; name: string; sprint: number; hour: number }; 299 title: string; 300 price: number; 301 total: number; 302 quantity: number; 303 description: string; 304 }[], 255 305 }); 256 306 257 307 // mutate current data 258 mutate([collections.invoice, currentInvoice.id]);308 // mutate([collections.invoice, currentInvoice.id]); 259 309 } else { 260 310 // generate collection id 261 311 const id = generateId(collections.invoice); 312 313 // Ensure dates are valid Date objects 314 const createDate = 315 data.createDate instanceof Date ? data.createDate : new Date(data.createDate); 316 const dueDate = data.dueDate instanceof Date ? data.dueDate : new Date(data.dueDate); 262 317 263 318 // attach serivce details … … 270 325 const writeData = { 271 326 ...data, 272 createDate : Timestamp.fromDate(new Date(data.createDate)),273 dueDate : Timestamp.fromDate(new Date(data.dueDate)),327 createDate, 328 dueDate, 274 329 items, 330 invoiceFrom: data.invoiceFrom!, 331 invoiceTo: data.invoiceTo!, 275 332 }; 276 333 … … 282 339 283 340 // write to DB 284 await firestoreBatch([ 285 { 286 docPath: `${collections.invoice}/${id}`, 287 type: 'set', 288 data: { 289 ...writeData, 290 pdfRef: storagePath, 291 }, 292 }, 293 { 294 docPath: `${collections.settings}/${documents.settingsInvoice}`, 295 type: 'set', 296 data: { lastInvoiceNumber: writeData.invoiceNumber }, 297 }, 298 ]); 341 // await firestoreBatch([ 342 // { 343 // docPath: `${collections.invoice}/${id}`, 344 // type: 'set', 345 // data: { 346 // ...writeData, 347 // pdfRef: storagePath, 348 // }, 349 // }, 350 // { 351 // docPath: `${collections.settings}/${documents.settingsInvoice}`, 352 // type: 'set', 353 // data: { lastInvoiceNumber: writeData.invoiceNumber }, 354 // }, 355 // ]); 356 await createInvoice({ ...writeData, pdfRef: storagePath }); 299 357 300 358 reset(); … … 314 372 <FormProvider methods={methods}> 315 373 <Card> 316 { settings?.company&& <InvoiceNewEditAddress />}374 {!!tenant && <InvoiceNewEditAddress />} 317 375 318 376 <InvoiceNewEditStatusDate isCopy={isCopy} /> -
src/sections/invoice/invoice-new-edit-status-date.tsx
r5d6f37a r057453c 7 7 import { RHFSelect, RHFTextField } from 'src/components/hook-form'; 8 8 // api 9 import { useGetSettings} from 'src/api/settings';9 // import { useGetTenant } from 'src/api/settings'; 10 10 // utils 11 11 import { incrementInvoiceNumber } from 'src/utils/increment-invoice-number'; … … 33 33 const values = watch(); 34 34 35 const { settings } = useGetSettings();35 // const { settings } = useGetTenant(); 36 36 37 37 return ( -
src/sections/invoice/invoice-pdf.tsx
r5d6f37a r057453c 1 import { Invoice } from ' mvpmasters-shared';1 import { Invoice } from 'src/schemas'; 2 2 import InvoiceEEPDF from './invoice-ee-pdf'; 3 3 import InvoiceMKPDF from './invoice-mk-pdf'; -
src/sections/invoice/invoice-table-filters-result.tsx
r5d6f37a r057453c 6 6 import Stack, { StackProps } from '@mui/material/Stack'; 7 7 // types 8 import { InvoiceTableFilters, InvoiceTableFilterValue } from ' mvpmasters-shared';8 import { InvoiceTableFilters, InvoiceTableFilterValue } from 'src/schemas'; 9 9 // components 10 10 import Iconify from 'src/components/iconify'; -
src/sections/invoice/invoice-table-row.tsx
r5d6f37a r057453c 16 16 import { fNumber } from 'src/utils/format-number'; 17 17 // types 18 import { Invoice } from ' mvpmasters-shared';18 import { Invoice } from 'src/schemas'; 19 19 // components 20 20 import Label from 'src/components/label'; … … 60 60 } = row; 61 61 62 console.log(createDate); 63 62 64 const confirmSend = useBoolean(); 63 65 const confirmDelete = useBoolean(); … … 105 107 <TableCell> 106 108 <ListItemText 107 primary={format( createDate.toDate(), 'dd MMM yyyy')}108 secondary={format( createDate.toDate(), 'p')}109 primary={format(new Date(createDate), 'dd MMM yyyy')} 110 secondary={format(new Date(createDate), 'p')} 109 111 primaryTypographyProps={{ typography: 'body2', noWrap: true }} 110 112 secondaryTypographyProps={{ … … 118 120 <TableCell> 119 121 <ListItemText 120 primary={format( dueDate.toDate(), 'dd MMM yyyy')}121 secondary={format( dueDate.toDate(), 'p')}122 primary={format(new Date(dueDate), 'dd MMM yyyy')} 123 secondary={format(new Date(dueDate), 'p')} 122 124 primaryTypographyProps={{ typography: 'body2', noWrap: true }} 123 125 secondaryTypographyProps={{ -
src/sections/invoice/invoice-table-toolbar.tsx
r5d6f37a r057453c 7 7 import InputAdornment from '@mui/material/InputAdornment'; 8 8 // types 9 import { InvoiceTableFilters, InvoiceTableFilterValue } from ' mvpmasters-shared';9 import { InvoiceTableFilters, InvoiceTableFilterValue } from 'src/schemas'; 10 10 // components 11 11 import Iconify from 'src/components/iconify'; -
src/sections/invoice/invoice-toolbar.tsx
r5d6f37a r057453c 18 18 import { useBoolean } from 'src/hooks/use-boolean'; 19 19 // types 20 import { Invoice } from ' mvpmasters-shared';20 import { Invoice } from 'src/schemas'; 21 21 // components 22 22 import Iconify from 'src/components/iconify'; -
src/sections/invoice/mail-compose.tsx
r5d6f37a r057453c 13 13 import Iconify from 'src/components/iconify'; 14 14 import Editor from 'src/components/editor'; 15 import { Invoice } from ' mvpmasters-shared';15 import { Invoice } from 'src/schemas'; 16 16 import FormProvider from 'src/components/hook-form/form-provider'; 17 17 import { RHFTextField } from 'src/components/hook-form'; -
src/sections/invoice/view/invoice-edit-view.tsx
r5d6f37a r057453c 23 23 24 24 const { currentInvoice } = useGetInvoice({ id }); 25 console.log('currentInvoice', currentInvoice); 25 26 26 27 return ( -
src/sections/invoice/view/invoice-list-view.tsx
r5d6f37a r057453c 36 36 } from 'src/components/table'; 37 37 // types 38 import { 39 Invoice, 40 InvoiceStatus, 41 InvoiceTableFilters, 42 InvoiceTableFilterValue, 43 } from 'mvpmasters-shared'; 38 import { Invoice, InvoiceStatus, InvoiceTableFilters, InvoiceTableFilterValue } from 'src/schemas'; 44 39 // 45 40 import deleteFromFirebaseStorage from 'src/utils/delete-from-firebase-storage'; 46 41 // fetch 47 import { use GetInvoices } from 'src/api/invoice';42 import { useDeleteInvoice, useGetInvoices } from 'src/api/invoice'; 48 43 import { collections, removeDocument } from 'src/lib/firestore'; 49 44 import { mutate } from 'swr'; … … 163 158 const [filters, setFilters] = useState(defaultFilters); 164 159 165 const { invoices: tableData } = useGetInvoices({ 166 where: [['createDate', '>=', filters.startDate]], 167 orderBy: 'createDate', 168 direction: 'desc', 169 }); 160 const { invoices: tableData } = useGetInvoices({ startDate: filters.startDate?.toISOString() }); 170 161 171 162 const invoiceMutationKey = useMemo( … … 269 260 ); 270 261 262 const { deleteInvoiceMutation } = useDeleteInvoice(); 263 271 264 const handleDeleteRow = useCallback( 272 265 async (invoice: Invoice) => { … … 275 268 orderBy: 'createDate', 276 269 direction: 'desc', 277 }); // Get the same params as used in useGetInvoices 278 279 // Optimistically update the cache before the deletion 280 // mutate( 281 // [collections.invoice, serializedParams], 282 // (invoices: Invoice[] = []) => invoices.filter((row) => row.id !== invoice.id), 283 // false 284 // ); 285 286 await removeDocument(collections.invoice, invoice.id); 270 }); 271 272 await deleteInvoiceMutation(invoice.id); 287 273 await deleteFromFirebaseStorage( 288 274 `invoices/${invoice.invoiceTo.name}/${invoice.id}-${invoice.invoiceNumber}.pdf` 289 275 ); 290 276 291 // Optionally, rollback optimistic update or refetch data292 277 mutate(invoiceMutationKey); 293 278 }, … … 672 657 inputData = inputData.filter( 673 658 (invoice) => 674 fTimestamp(invoice.createDate. toMillis()) >= fTimestamp(startDate) &&675 fTimestamp(invoice.createDate. toMillis()) <= fTimestamp(endDate)659 fTimestamp(invoice.createDate.getTime()) >= fTimestamp(startDate.getTime()) && 660 fTimestamp(invoice.createDate.getTime()) <= fTimestamp(endDate.getTime()) 676 661 ); 677 662 } -
src/sections/user/customer-new-edit-form.tsx
r5d6f37a r057453c 18 18 import { useRouter } from 'src/routes/hooks'; 19 19 // types 20 import { Customer, NewCustomer, newCustomerSchema } from ' mvpmasters-shared';20 import { Customer, NewCustomer, newCustomerSchema } from 'src/schemas'; 21 21 // components 22 22 import Label from 'src/components/label'; -
src/sections/user/customer-quick-edit-form.tsx
r5d6f37a r057453c 14 14 import DialogContent from '@mui/material/DialogContent'; 15 15 // types 16 import { Customer, customerSchema } from ' mvpmasters-shared';16 import { Customer, customerSchema } from 'src/schemas'; 17 17 // assets 18 18 import { countries } from 'src/assets/data'; -
src/sections/user/customer-table-filters-result.tsx
r5d6f37a r057453c 9 9 // components 10 10 import Iconify from 'src/components/iconify'; 11 import { CustomerTableFilterValue, CustomerTableFilters } from ' mvpmasters-shared';11 import { CustomerTableFilterValue, CustomerTableFilters } from 'src/schemas'; 12 12 13 13 // ---------------------------------------------------------------------- -
src/sections/user/customer-table-row.tsx
r5d6f37a r057453c 9 9 import { useBoolean } from 'src/hooks/use-boolean'; 10 10 // types 11 import { Customer } from ' mvpmasters-shared';11 import { Customer } from 'src/schemas'; 12 12 // components 13 13 import Label from 'src/components/label'; -
src/sections/user/customer-table-toolbar.tsx
r5d6f37a r057453c 12 12 import Select, { SelectChangeEvent } from '@mui/material/Select'; 13 13 // types 14 import { CustomerTableFilters, CustomerTableFilterValue } from ' mvpmasters-shared';14 import { CustomerTableFilters, CustomerTableFilterValue } from 'src/schemas'; 15 15 // components 16 16 import Iconify from 'src/components/iconify'; -
src/sections/user/view/customer-list-view.tsx
r5d6f37a r057453c 34 34 } from 'src/components/table'; 35 35 // types 36 import { Customer, CustomerTableFilters, CustomerTableFilterValue } from ' mvpmasters-shared';36 import { Customer, CustomerTableFilters, CustomerTableFilterValue } from 'src/schemas'; 37 37 // 38 38 import { useGetCustomers } from 'src/api/customer'; -
src/utils/axios.ts
r5d6f37a r057453c 45 45 46 46 export const endpoints = { 47 invoice: '/api/invoice ',47 invoice: '/api/invoices', 48 48 customer: '/api/customers', 49 tenant: '/api/tenant', 50 service: '/api/services', 51 employee: '/api/employees', 49 52 }; -
src/utils/create-full-address.ts
r5d6f37a r057453c 1 import { Address } from ' mvpmasters-shared';1 import { Address } from 'src/schemas'; 2 2 3 3 export function createFullAddress(data: Address): string { -
src/utils/get-invoice-quantity-type.ts
r5d6f37a r057453c 1 import { Invoice } from ' mvpmasters-shared';1 import { Invoice } from 'src/schemas'; 2 2 3 3 export const getQuantityType = (invoice: Invoice) => { -
yarn.lock
r5d6f37a r057453c 1534 1534 integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg== 1535 1535 1536 "@fastify/busboy@^2.0.0":1537 version "2.1.1"1538 resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"1539 integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==1540 1541 1536 "@fastify/busboy@^3.0.0": 1542 1537 version "3.1.1" … … 1640 1635 tslib "^2.1.0" 1641 1636 1642 "@firebase/app@^0.9.25":1643 version "0.9.29"1644 resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.29.tgz#444280f0ddf1da4b2a974c86a6a8c6405d950fb7"1645 integrity sha512-HbKTjfmILklasIu/ij6zKnFf3SgLYXkBDVN7leJfVGmohl+zA7Ig+eXM1ZkT1pyBJ8FTYR+mlOJer/lNEnUCtw==1646 dependencies:1647 "@firebase/component" "0.6.5"1648 "@firebase/logger" "0.4.0"1649 "@firebase/util" "1.9.4"1650 idb "7.1.1"1651 tslib "^2.1.0"1652 1653 1637 "@firebase/auth-compat@0.4.6": 1654 1638 version "0.4.6" … … 1703 1687 dependencies: 1704 1688 "@firebase/util" "1.9.3" 1705 tslib "^2.1.0"1706 1707 "@firebase/component@0.6.5":1708 version "0.6.5"1709 resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.5.tgz#8cc7334f2081d700f2769caaa8dae3ac4c1fe37e"1710 integrity sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==1711 dependencies:1712 "@firebase/util" "1.9.4"1713 tslib "^2.1.0"1714 1715 "@firebase/component@0.6.6":1716 version "0.6.6"1717 resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.6.tgz#7ad4013ff37686d355dee0694f1fa4604491b7c3"1718 integrity sha512-pp7sWqHmAAlA3os6ERgoM3k5Cxff510M9RLXZ9Mc8KFKMBc2ct3RkZTWUF7ixJNvMiK/iNgRLPDrLR2gtRJ9iQ==1719 dependencies:1720 "@firebase/util" "1.9.5"1721 1689 tslib "^2.1.0" 1722 1690 … … 1801 1769 resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.0.tgz#f3440d5a1cc2a722d361b24cefb62ca8b3577af3" 1802 1770 integrity sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw== 1803 1804 "@firebase/firestore-types@^3.0.0":1805 version "3.0.1"1806 resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.1.tgz#5b802f8aeffe6803f594b428054b10e25a73ae66"1807 integrity sha512-mVhPcHr5FICjF67m6JHgj+XRvAz/gZ62xifeGfcm00RFl6tNKfCzCfKeyB2BDIEc9dUnEstkmIXlmLIelOWoaA==1808 1771 1809 1772 "@firebase/firestore@4.2.0": … … 1821 1784 tslib "^2.1.0" 1822 1785 1823 "@firebase/firestore@^4.4.0":1824 version "4.6.1"1825 resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.6.1.tgz#4a788103bca46e3ee829ddb816189f23cd26212a"1826 integrity sha512-MaBOBu+QcZOp6SJzCmigiJ4Dt0HNic91w8GghbTE9L//VW/zdO7ezXrcXRK4TjWWOcazBrJZJSHTIsFdwZyvtQ==1827 dependencies:1828 "@firebase/component" "0.6.6"1829 "@firebase/logger" "0.4.1"1830 "@firebase/util" "1.9.5"1831 "@firebase/webchannel-wrapper" "0.10.6"1832 "@grpc/grpc-js" "~1.9.0"1833 "@grpc/proto-loader" "^0.7.8"1834 tslib "^2.1.0"1835 undici "5.28.4"1836 1837 1786 "@firebase/functions-compat@0.3.5": 1838 1787 version "0.3.5" … … 1897 1846 tslib "^2.1.0" 1898 1847 1899 "@firebase/logger@0.4.1":1900 version "0.4.1"1901 resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.1.tgz#b4bb0d266680210a34e7c70a8a92342391e399ab"1902 integrity sha512-tTIixB5UJbG9ZHSGZSZdX7THr3KWOLrejZ9B7jYsm6fpwgRNngKznQKA2wgYVyvBc1ta7dGFh9NtJ8n7qfiYIw==1903 dependencies:1904 tslib "^2.1.0"1905 1906 1848 "@firebase/logger@0.4.4": 1907 1849 version "0.4.4" … … 2034 1976 tslib "^2.1.0" 2035 1977 2036 "@firebase/util@1.9.4":2037 version "1.9.4"2038 resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.4.tgz#68eee380ab7e7828ec0d8684c46a1abed2d7e334"2039 integrity sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==2040 dependencies:2041 tslib "^2.1.0"2042 2043 "@firebase/util@1.9.5", "@firebase/util@^1.5.1":2044 version "1.9.5"2045 resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.5.tgz#9321fc1695b30a9283c99f768da2e837e02cbddc"2046 integrity sha512-PP4pAFISDxsf70l3pEy34Mf3GkkUcVQ3MdKp6aSVb7tcpfUQxnsdV7twDd8EkfB6zZylH6wpUAoangQDmCUMqw==2047 dependencies:2048 tslib "^2.1.0"2049 2050 1978 "@firebase/webchannel-wrapper@0.10.3": 2051 1979 version "0.10.3" 2052 1980 resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.3.tgz#c894a21e8c911830e36bbbba55903ccfbc7a7e25" 2053 1981 integrity sha512-+ZplYUN3HOpgCfgInqgdDAbkGGVzES1cs32JJpeqoh87SkRobGXElJx+1GZSaDqzFL+bYiX18qEcBK76mYs8uA== 2054 2055 "@firebase/webchannel-wrapper@0.10.6":2056 version "0.10.6"2057 resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.6.tgz#443efa9f761fd4ed8ca60c353e3a44d3a47e81f1"2058 integrity sha512-EnfRJvrnzkHwN3BPMCayCFT5lCqInzg3RdlRsDjDvB1EJli6Usj26T6lJ67BU2UcYXBS5xcp1Wj4+zRzj2NaZg==2059 1982 2060 1983 "@floating-ui/core@^1.4.2": … … 7474 7397 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 7475 7398 7476 moment@^2.29.4:7477 version "2.30.1"7478 resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"7479 integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==7480 7481 7399 ms@2.0.0: 7482 7400 version "2.0.0" … … 7516 7434 arrify "^2.0.1" 7517 7435 minimatch "^3.0.4" 7518 7519 mvpmasters-shared@^1.0.9:7520 version "1.0.9"7521 resolved "https://registry.yarnpkg.com/mvpmasters-shared/-/mvpmasters-shared-1.0.9.tgz#964127bb04e25dcdfceab1aa490e106300bf430d"7522 integrity sha512-RluBz76NR5Q2yd25nWgKIuPAWHUa75U4uwR6e7iFw6zFIn+b83fE0upbUY7+iRFF5SSTFfj/7Uk5zZffy1HLlQ==7523 dependencies:7524 "@firebase/app" "^0.9.25"7525 "@firebase/firestore" "^4.4.0"7526 "@firebase/firestore-types" "^3.0.0"7527 "@firebase/util" "^1.5.1"7528 lodash "^4.17.21"7529 moment "^2.29.4"7530 zod "^3.22.4"7531 7436 7532 7437 nanoid@^3.3.6: … … 9513 9418 is-typed-array "^1.1.9" 9514 9419 9515 typescript@ ^5.2.2:9516 version "5. 2.2"9517 resolved "https://registry.yarnpkg.com/typescript/-/typescript-5. 2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"9518 integrity sha512- mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==9420 typescript@5.7.3: 9421 version "5.7.3" 9422 resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" 9423 integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== 9519 9424 9520 9425 unbox-primitive@^1.0.2: … … 9532 9437 resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" 9533 9438 integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== 9534 9535 undici@5.28.4:9536 version "5.28.4"9537 resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"9538 integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==9539 dependencies:9540 "@fastify/busboy" "^2.0.0"9541 9439 9542 9440 unicode-canonical-property-names-ecmascript@^2.0.0: … … 9957 9855 resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.2.tgz#068606642c8f51b3333981f91c0a8ab37dfc2807" 9958 9856 integrity sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ== 9959 9960 zod@^3.22.4:9961 version "3.23.5"9962 resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.5.tgz#c7b7617d017d4a2f21852f533258d26a9a5ae09f"9963 integrity sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==
Note:
See TracChangeset
for help on using the changeset viewer.