├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── actions │ ├── client-profile.ts │ └── products.ts ├── api │ ├── chat │ │ └── route.ts │ ├── client-documents │ │ └── route.ts │ ├── clients │ │ └── [id] │ │ │ └── route.ts │ ├── contacts │ │ ├── route.ts │ │ └── tags │ │ │ └── route.ts │ ├── documents │ │ ├── route.ts │ │ └── search │ │ │ └── route.ts │ ├── interactions │ │ └── route.ts │ ├── products │ │ └── route.ts │ ├── proposals │ │ └── route.ts │ ├── retell │ │ ├── calls │ │ │ └── route.ts │ │ ├── create-call │ │ │ └── route.ts │ │ ├── phone-numbers │ │ │ └── route.ts │ │ ├── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── sales │ │ ├── products │ │ │ └── route.ts │ │ └── proposals │ │ │ └── route.ts │ ├── search │ │ └── route.ts │ ├── storage │ │ └── contacts-data │ │ │ └── route.ts │ └── twilio │ │ ├── calls │ │ └── route.ts │ │ ├── messages │ │ └── route.ts │ │ ├── phone-numbers │ │ └── route.ts │ │ └── webhook │ │ └── route.ts ├── auth │ ├── actions.ts │ └── callback │ │ └── route.ts ├── dashboard │ ├── ai-agent │ │ ├── components │ │ │ ├── ai-agent-chat.tsx │ │ │ └── ai-agent-header.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── clients │ │ └── [id] │ │ │ └── page.tsx │ ├── contacts │ │ ├── [contactId] │ │ │ └── documents │ │ │ │ ├── page.tsx │ │ │ │ └── upload-document-form.tsx │ │ ├── add-contact-dialog.tsx │ │ ├── add-contact-form.tsx │ │ ├── columns.tsx │ │ ├── contacts-client.tsx │ │ ├── edit-contact-dialog.tsx │ │ ├── empty-state.tsx │ │ ├── error-state.tsx │ │ ├── import-contact-dialog.tsx │ │ ├── import-contact-form.tsx │ │ ├── loading-state.tsx │ │ ├── page.tsx │ │ ├── tag-filter.tsx │ │ └── toolbar.tsx │ ├── layout.tsx │ ├── messages │ │ └── email │ │ │ └── page.tsx │ ├── page.tsx │ ├── products │ │ ├── [id] │ │ │ └── edit │ │ │ │ └── page.tsx │ │ ├── actions.ts │ │ ├── advanced-search.tsx │ │ ├── delete-product-dialog.tsx │ │ ├── new │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── product-card.tsx │ │ ├── product-form.tsx │ │ ├── products-header.tsx │ │ ├── products-list.tsx │ │ └── products-search.tsx │ ├── profile │ │ └── page.tsx │ ├── retell │ │ └── page.tsx │ ├── sales │ │ ├── analysis │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── products │ │ │ ├── page.tsx │ │ │ └── product-form.tsx │ │ └── proposals │ │ │ ├── actions.ts │ │ │ ├── components │ │ │ ├── company-details.tsx │ │ │ ├── contact-selector.tsx │ │ │ ├── pdf-generator.tsx │ │ │ ├── product-selector.tsx │ │ │ ├── proposal-form.tsx │ │ │ ├── proposal-table.tsx │ │ │ ├── proposals-table.tsx │ │ │ ├── proposals-wrapper.tsx │ │ │ └── statistics-dashboard.tsx │ │ │ ├── create │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── storage │ │ ├── contacts-data │ │ │ └── page.tsx │ │ ├── documents │ │ │ ├── page.tsx │ │ │ ├── preview-content.tsx │ │ │ └── upload-document-form.tsx │ │ ├── page.tsx │ │ └── products │ │ │ └── page.tsx │ └── twilio │ │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx ├── lib │ └── schemas │ │ └── contact-schema.ts ├── login │ ├── action.ts │ └── page.tsx ├── mocks │ └── twilioMockData.ts ├── page.tsx ├── providers.tsx ├── services │ └── contact-transformer.ts └── types │ ├── client-documents.ts │ ├── client-profile.ts │ ├── contact.ts │ ├── document.ts │ ├── email.ts │ ├── filters.ts │ ├── interaction.ts │ ├── pagination.ts │ ├── product.ts │ ├── proposal.ts │ ├── retell.ts │ ├── sales.ts │ └── twilio.ts ├── components.json ├── components ├── app-sidebar │ ├── app-sidebar-data.ts │ ├── app-sidebar-wrapper.tsx │ ├── app-sidebar.tsx │ ├── nav-main.tsx │ ├── nav-projects.tsx │ ├── nav-user.tsx │ └── team-switcher.tsx ├── auth │ └── login-form.tsx ├── client-profile │ ├── client-header.tsx │ ├── client-profile.tsx │ ├── dialogs │ │ └── add-interaction-dialog.tsx │ ├── edit-form.tsx │ ├── edit-profile-dialog.tsx │ └── tabs │ │ ├── ai-tab.tsx │ │ ├── documents-tab.tsx │ │ ├── interactions-tab.tsx │ │ ├── overview-tab.tsx │ │ └── sales-tab.tsx ├── contacts │ └── contacts-table.tsx ├── email │ ├── EmailCompose.tsx │ ├── EmailContent.tsx │ ├── EmailDetail.tsx │ ├── EmailSidebar.tsx │ ├── EmailSummary.tsx │ ├── UserMenu.tsx │ └── email.tsx ├── error-handler.tsx ├── navigation │ └── sidebar.tsx ├── retell │ ├── CallInitiator.tsx │ ├── CallList.tsx │ ├── CallModal.tsx │ ├── CallStatus.tsx │ └── Overview.tsx ├── sales │ └── StatCard.tsx ├── twilio │ ├── TwilioCallForm.tsx │ ├── TwilioCallStatus.tsx │ ├── TwilioMessageList.tsx │ ├── TwilioOverview.tsx │ ├── TwilioPhoneNumberList.tsx │ └── TwilioStatisticsCards.tsx └── ui │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── card.tsx │ ├── chart.tsx │ ├── chat │ ├── chat-bubble.tsx │ ├── chat-input.tsx │ ├── chat-message-list.tsx │ ├── expandable-chat.tsx │ └── message-loading.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── data-table.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── modal.tsx │ ├── multi-select.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── view-modal.tsx ├── hooks ├── use-ai-chat.ts ├── use-auth-state.ts ├── use-client-documents.ts ├── use-client-profile.ts ├── use-contacts-data.ts ├── use-contacts.ts ├── use-debounce.ts ├── use-documents.ts ├── use-interactions.ts ├── use-knowledge-items.ts ├── use-mobile.tsx ├── use-products.ts ├── use-proposals.ts ├── use-scripts.ts ├── use-search-with-debounce.ts ├── use-search.ts └── use-toast.ts ├── lib ├── together-ai.ts ├── twilio.ts └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── sidebar.md ├── tailwind.config.ts ├── tsconfig.json ├── types └── auth.ts └── utils ├── company.ts ├── embeddings.ts ├── pdf.ts ├── supabase ├── client.ts ├── middleware.ts └── server.ts └── validation.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /app/actions/client-profile.ts: -------------------------------------------------------------------------------- 1 | import { SupabaseClient } from '@supabase/supabase-js' 2 | 3 | export async function initializeClientProfile( 4 | supabase: SupabaseClient, 5 | contactId: string, 6 | userId: string 7 | ) { 8 | try { 9 | const { data, error } = await supabase 10 | .rpc('initialize_client_profile', { 11 | p_contact_id: contactId, 12 | p_user_id: userId 13 | }) 14 | 15 | if (error) { 16 | console.error('Error in initializeClientProfile:', error) 17 | throw error 18 | } 19 | 20 | return data 21 | } catch (error) { 22 | console.error('Failed to initialize client profile:', error) 23 | throw error 24 | } 25 | } -------------------------------------------------------------------------------- /app/actions/products.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { createClient } from "@/utils/supabase/server" 4 | import { cookies } from "next/headers" 5 | import { revalidatePath } from "next/cache" 6 | import { Product } from "@/app/types/product" 7 | 8 | export async function createProduct(data: Partial) { 9 | const cookieStore = cookies() 10 | const supabase = createClient(cookieStore) 11 | 12 | try { 13 | // 1. Vérifier l'utilisateur connecté 14 | const { data: { user }, error: userError } = await supabase.auth.getUser() 15 | if (userError || !user) { 16 | throw new Error("Unauthorized") 17 | } 18 | 19 | // 2. Préparer les données 20 | const productData = { 21 | ...data, 22 | user_id: user.id, 23 | created_at: new Date().toISOString(), 24 | updated_at: new Date().toISOString() 25 | } 26 | 27 | // 3. Insérer le produit 28 | const { error } = await supabase 29 | .from("products") 30 | .insert([productData]) 31 | 32 | if (error) { 33 | console.error("Insertion error:", error) 34 | throw error 35 | } 36 | 37 | revalidatePath("/dashboard/products") 38 | return { success: true } 39 | 40 | } catch (error) { 41 | console.error("Erreur détaillée:", error) 42 | throw error 43 | } 44 | } 45 | 46 | export async function updateProduct(id: string, data: Partial) { 47 | const cookieStore = cookies() 48 | const supabase = createClient(cookieStore) 49 | 50 | // Mise à jour du produit sans embedding 51 | const { error } = await supabase 52 | .from("products") 53 | .update({ 54 | ...data, 55 | updated_at: new Date().toISOString() 56 | }) 57 | .eq("id", id) 58 | 59 | if (error) throw error 60 | 61 | revalidatePath("/dashboard/products") 62 | } 63 | 64 | export async function deleteProduct(id: string) { 65 | const cookieStore = cookies() 66 | const supabase = createClient(cookieStore) 67 | 68 | const { error } = await supabase 69 | .from("products") 70 | .delete() 71 | .eq("id", id) 72 | 73 | if (error) throw error 74 | 75 | revalidatePath("/dashboard/products") 76 | } -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/utils/supabase/server' 2 | import { StreamingTextResponse, Message } from 'ai' 3 | import { togetherAI, TOGETHER_MODEL } from '@/lib/together-ai' 4 | import { cookies } from 'next/headers' 5 | 6 | export async function POST(req: Request) { 7 | const supabase = createClient(cookies()) 8 | const { messages, conversationId } = await req.json() 9 | 10 | const { data: { user }, error: authError } = await supabase.auth.getUser() 11 | if (authError || !user) { 12 | return new Response('Unauthorized', { status: 401 }) 13 | } 14 | 15 | try { 16 | const stream = await togetherAI.chat.completions.create({ 17 | model: TOGETHER_MODEL, 18 | messages: messages.map(({ content, role }: Message) => ({ 19 | content, 20 | role: role === 'user' ? 'user' : 'assistant' 21 | })), 22 | stream: true, 23 | temperature: 0.7, 24 | max_tokens: 1000 25 | }) 26 | 27 | // Convert OpenAI stream to ReadableStream 28 | const readableStream = new ReadableStream({ 29 | async start(controller) { 30 | for await (const chunk of stream) { 31 | const text = chunk.choices[0]?.delta?.content || '' 32 | if (text) { 33 | controller.enqueue(text) 34 | } 35 | } 36 | controller.close() 37 | } 38 | }) 39 | 40 | // Stockage du message dans Supabase 41 | if (conversationId) { 42 | await supabase.from('ai_messages').insert({ 43 | conversation_id: conversationId, 44 | role: 'user', 45 | content: messages[messages.length - 1].content, 46 | user_id: user.id, 47 | timestamp: new Date().toISOString() 48 | }) 49 | } 50 | 51 | return new StreamingTextResponse(readableStream) 52 | } catch (error) { 53 | console.error('Chat error:', error) 54 | return new Response('Error processing your request', { status: 500 }) 55 | } 56 | } -------------------------------------------------------------------------------- /app/api/clients/[id]/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/clients/[id]/route.ts 2 | import { createClient } from "@/utils/supabase/server" 3 | import { cookies } from "next/headers" 4 | import { NextResponse } from "next/server" 5 | import { clientProfileUpdateSchema } from "@/utils/validation" 6 | import type { ClientProfile } from "@/app/types/client-profile" 7 | 8 | export async function PATCH( 9 | request: Request, 10 | { params }: { params: { id: string } } 11 | ) { 12 | try { 13 | const cookieStore = cookies() 14 | const supabase = createClient(cookieStore) 15 | 16 | const body = await request.json() 17 | const validatedData = clientProfileUpdateSchema.parse(body) 18 | 19 | // Mise à jour du client dans la base de données 20 | const { data, error } = await supabase 21 | .from('contacts') 22 | .update({ 23 | ...validatedData, 24 | updated_at: new Date().toISOString(), 25 | }) 26 | .eq('id', params.id) 27 | .select() 28 | .single() 29 | 30 | if (error) { 31 | throw error 32 | } 33 | 34 | return NextResponse.json(data) 35 | } catch (error) { 36 | console.error('Error updating client:', error) 37 | return NextResponse.json( 38 | { error: 'Failed to update client' }, 39 | { status: 500 } 40 | ) 41 | } 42 | } 43 | 44 | export async function GET( 45 | request: Request, 46 | { params }: { params: { id: string } } 47 | ) { 48 | try { 49 | const cookieStore = cookies() 50 | const supabase = createClient(cookieStore) 51 | 52 | const { data, error } = await supabase 53 | .from('contacts') 54 | .select(` 55 | *, 56 | company:companies( 57 | id, 58 | name, 59 | industry, 60 | website, 61 | linkedin 62 | ) 63 | `) 64 | .eq('id', params.id) 65 | .single() 66 | 67 | if (error) { 68 | throw error 69 | } 70 | 71 | return NextResponse.json(data as ClientProfile) 72 | } catch (error) { 73 | console.error('Error fetching client:', error) 74 | return NextResponse.json( 75 | { error: 'Failed to fetch client' }, 76 | { status: 500 } 77 | ) 78 | } 79 | } -------------------------------------------------------------------------------- /app/api/contacts/tags/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/contacts/tags/route.ts 2 | import { createClient } from "@/utils/supabase/server" 3 | import { cookies } from "next/headers" 4 | import { NextResponse } from "next/server" 5 | import { SupabaseContact } from "@/app/types/contact" 6 | 7 | export async function GET() { 8 | const cookieStore = cookies() 9 | const supabase = createClient(cookieStore) 10 | 11 | const { data, error } = await supabase 12 | .from('contacts') 13 | .select('tags') 14 | .not('tags', 'is', null) as { 15 | data: Pick[] | null; 16 | error: Error | null; 17 | } 18 | 19 | if (error) { 20 | return NextResponse.json({ error: error.message }, { status: 500 }) 21 | } 22 | 23 | if (!data) { 24 | return NextResponse.json([]) 25 | } 26 | 27 | const uniqueTags = Array.from( 28 | new Set(data.flatMap(contact => contact.tags || [])) 29 | ) 30 | 31 | return NextResponse.json(uniqueTags) 32 | } -------------------------------------------------------------------------------- /app/api/documents/search/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | import { generateEmbeddings } from "@/utils/embeddings" 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const cookieStore = cookies() 9 | const supabase = createClient(cookieStore) 10 | 11 | const { query } = await request.json() 12 | const queryEmbedding = await generateEmbeddings(query) 13 | 14 | const { data, error } = await supabase.rpc('match_documents', { 15 | query_embedding: queryEmbedding, 16 | match_threshold: 0.78, 17 | match_count: 5 18 | }) 19 | 20 | if (error) throw error 21 | 22 | return NextResponse.json({ data }) 23 | } catch (error) { 24 | console.error('Document search failed:', error); 25 | return NextResponse.json( 26 | { error: error instanceof Error ? error.message : "Search failed" }, 27 | { status: 500 } 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /app/api/interactions/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | import { interactionSchema } from "@/app/types/interaction" 5 | 6 | export async function GET(request: Request) { 7 | try { 8 | const cookieStore = cookies() 9 | const supabase = createClient(cookieStore) 10 | 11 | const { searchParams } = new URL(request.url) 12 | const contactId = searchParams.get('contact_id') 13 | 14 | if (!contactId) { 15 | return NextResponse.json({ error: "contact_id is required" }, { status: 400 }) 16 | } 17 | 18 | const { data, error } = await supabase 19 | .from('interactions') 20 | .select('*') 21 | .eq('contact_id', contactId) 22 | .order('date', { ascending: false }) 23 | 24 | if (error) throw error 25 | 26 | return NextResponse.json(data) 27 | } catch (error) { 28 | console.error('Error fetching interactions:', error) 29 | return NextResponse.json( 30 | { error: 'Failed to fetch interactions' }, 31 | { status: 500 } 32 | ) 33 | } 34 | } 35 | 36 | export async function POST(request: Request) { 37 | try { 38 | const cookieStore = cookies() 39 | const supabase = createClient(cookieStore) 40 | 41 | const body = await request.json() 42 | const validatedData = interactionSchema.parse(body) 43 | 44 | const { data, error } = await supabase 45 | .from('interactions') 46 | .insert([{ 47 | ...validatedData, 48 | created_at: new Date().toISOString(), 49 | updated_at: new Date().toISOString() 50 | }]) 51 | .select() 52 | .single() 53 | 54 | if (error) throw error 55 | 56 | return NextResponse.json(data, { status: 201 }) 57 | } catch (error) { 58 | console.error('Error creating interaction:', error) 59 | return NextResponse.json( 60 | { error: 'Failed to create interaction' }, 61 | { status: 500 } 62 | ) 63 | } 64 | } -------------------------------------------------------------------------------- /app/api/products/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/products/route.ts 2 | import type { SupabaseResponse, Product } from "@/app/types/product" 3 | import { createClient } from "@/utils/supabase/server" 4 | import { cookies } from "next/headers" 5 | import { NextRequest, NextResponse } from "next/server" 6 | import { generateEmbeddings } from "@/utils/embeddings" 7 | 8 | export const dynamic = 'force-dynamic' 9 | 10 | export async function GET(request: NextRequest) { 11 | const searchParams = request.nextUrl.searchParams 12 | const query = searchParams.get('query') 13 | const category = searchParams.get('category') 14 | 15 | const cookieStore = cookies() 16 | const supabase = createClient(cookieStore) 17 | 18 | if (query) { 19 | const embedding = await generateEmbeddings(query) 20 | const { data, error } = await supabase.rpc('search_products', { 21 | query_embedding: embedding, 22 | match_threshold: 0.8, 23 | match_count: 10 24 | }) as SupabaseResponse 25 | if (error) throw error 26 | return NextResponse.json(data) 27 | } 28 | 29 | // Regular search with filters 30 | let productsQuery = supabase.from('products').select('*') 31 | if (category) { 32 | productsQuery = productsQuery.eq('category', category) 33 | } 34 | 35 | const { data, error } = await productsQuery as SupabaseResponse 36 | if (error) throw error 37 | return NextResponse.json(data) 38 | } -------------------------------------------------------------------------------- /app/api/proposals/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/utils/supabase/server' 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | import { proposalSchema } from "@/app/types/proposal" 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const cookieStore = cookies() 9 | const supabase = await createClient(cookieStore) 10 | const { data: { user }, error: userError } = await supabase.auth.getUser() 11 | 12 | if (userError || !user) { 13 | return NextResponse.json( 14 | { error: "Non autorisé" }, 15 | { status: 401 } 16 | ) 17 | } 18 | 19 | const data = await request.json() 20 | const validation = proposalSchema.safeParse(data) 21 | 22 | if (!validation.success) { 23 | return NextResponse.json( 24 | { error: validation.error.format() }, 25 | { status: 400 } 26 | ) 27 | } 28 | 29 | const { data: proposal, error } = await supabase 30 | .from("client_proposals") 31 | .insert([ 32 | { 33 | ...validation.data, 34 | user_id: user.id, 35 | created_at: new Date().toISOString(), 36 | updated_at: new Date().toISOString(), 37 | }, 38 | ]) 39 | .select() 40 | .single() 41 | 42 | if (error) throw error 43 | 44 | return NextResponse.json(proposal) 45 | } catch (error) { 46 | console.error("Erreur lors de la création du devis:", error) 47 | return NextResponse.json( 48 | { error: "Erreur lors de la création du devis" }, 49 | { status: 500 } 50 | ) 51 | } 52 | } 53 | 54 | export async function GET() { 55 | try { 56 | const cookieStore = cookies() 57 | const supabase = await createClient(cookieStore) 58 | const { data: { user }, error: userError } = await supabase.auth.getUser() 59 | 60 | if (userError || !user) { 61 | return NextResponse.json( 62 | { error: "Non autorisé" }, 63 | { status: 401 } 64 | ) 65 | } 66 | 67 | const { data: proposals, error } = await supabase 68 | .from("client_proposals") 69 | .select("*") 70 | .order("created_at", { ascending: false }) 71 | 72 | if (error) throw error 73 | 74 | return NextResponse.json(proposals) 75 | } catch (error) { 76 | console.error("Erreur lors de la récupération des devis:", error) 77 | return NextResponse.json( 78 | { error: "Erreur lors de la récupération des devis" }, 79 | { status: 500 } 80 | ) 81 | } 82 | } -------------------------------------------------------------------------------- /app/api/retell/calls/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { RetellCall } from '@/app/types/retell' 3 | 4 | export async function GET() { 5 | const mockCalls: RetellCall[] = Array.from({ length: 10 }, (_, index) => 6 | generateMockCall(index + 1) 7 | ); 8 | 9 | return NextResponse.json(mockCalls) 10 | } 11 | 12 | export async function POST(request: Request) { 13 | const body = await request.json(); 14 | // Implement POST logic here 15 | return NextResponse.json({ message: "POST request received", data: body }) 16 | } 17 | 18 | function generateMockCall(id: number): RetellCall { 19 | const startTime = Date.now() - Math.floor(Math.random() * 3600000) // Jusqu'à 1 heure dans le passé 20 | const endTime = startTime + Math.floor(Math.random() * 600000) // Jusqu'à 10 minutes de durée 21 | 22 | return { 23 | call_type: Math.random() > 0.5 ? 'inbound' : 'outbound', 24 | call_id: `call_${id}`, 25 | agent_id: `agent_${Math.floor(Math.random() * 5) + 1}`, 26 | call_status: ['completed', 'in_progress', 'failed'][Math.floor(Math.random() * 3)], 27 | metadata: {}, 28 | retell_llm_dynamic_variables: { 29 | customer_name: `Customer ${id}`, 30 | }, 31 | opt_out_sensitive_data_storage: false, 32 | start_timestamp: startTime, 33 | end_timestamp: endTime, 34 | transcript: `This is a mock transcript for call ${id}`, 35 | transcript_object: [ 36 | { 37 | role: 'agent', 38 | content: 'Hello, how can I help you today?', 39 | words: [], 40 | }, 41 | { 42 | role: 'customer', 43 | content: 'I have a question about my account.', 44 | words: [], 45 | }, 46 | ], 47 | recording_url: `https://example.com/recording_${id}.mp3`, 48 | public_log_url: `https://example.com/log_${id}.txt`, 49 | e2e_latency: { 50 | p50: 100, 51 | p90: 200, 52 | p95: 250, 53 | p99: 300, 54 | max: 350, 55 | min: 50, 56 | num: 100, 57 | }, 58 | llm_latency: { 59 | p50: 50, 60 | p90: 100, 61 | p95: 125, 62 | p99: 150, 63 | max: 175, 64 | min: 25, 65 | num: 100, 66 | }, 67 | llm_websocket_network_rtt_latency: { 68 | p50: 10, 69 | p90: 20, 70 | p95: 25, 71 | p99: 30, 72 | max: 35, 73 | min: 5, 74 | num: 100, 75 | }, 76 | disconnection_reason: '', 77 | call_analysis: { 78 | call_summary: `This is a summary for call ${id}`, 79 | in_voicemail: false, 80 | user_sentiment: ['positive', 'neutral', 'negative'][Math.floor(Math.random() * 3)], 81 | call_successful: Math.random() > 0.2, 82 | custom_analysis_data: {}, 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/api/retell/create-call/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { RetellCall } from '@/app/types/retell'; 3 | 4 | export async function POST(request: Request) { 5 | const body = await request.json(); 6 | const { from_number, to_number } = body; 7 | 8 | if (!from_number || !to_number) { 9 | return NextResponse.json({ error: 'From number and to number are required' }, { status: 400 }); 10 | } 11 | 12 | const mockCall: RetellCall = { 13 | call_type: "outbound", 14 | call_id: Math.random().toString(36).substring(7), 15 | agent_id: "agent_123", 16 | call_status: "initiated", 17 | metadata: {}, 18 | retell_llm_dynamic_variables: { 19 | customer_name: "John Doe" 20 | }, 21 | opt_out_sensitive_data_storage: false, 22 | start_timestamp: Date.now(), 23 | end_timestamp: Date.now() + 300000, // 5 minutes later 24 | transcript: "This is a mock transcript.", 25 | transcript_object: [ 26 | { 27 | role: "agent", 28 | content: "Hello, this is a mock conversation.", 29 | words: [ 30 | { word: "Hello,", start: 0, end: 0.5 }, 31 | { word: "this", start: 0.6, end: 0.8 }, 32 | { word: "is", start: 0.9, end: 1.0 }, 33 | { word: "a", start: 1.1, end: 1.2 }, 34 | { word: "mock", start: 1.3, end: 1.5 }, 35 | { word: "conversation.", start: 1.6, end: 2.0 } 36 | ] 37 | } 38 | ], 39 | recording_url: "https://example.com/mock-recording.mp3", 40 | public_log_url: "https://example.com/mock-public-log", 41 | e2e_latency: { 42 | p50: 100, p90: 150, p95: 200, p99: 250, max: 300, min: 50, num: 100 43 | }, 44 | llm_latency: { 45 | p50: 50, p90: 75, p95: 100, p99: 125, max: 150, min: 25, num: 100 46 | }, 47 | llm_websocket_network_rtt_latency: { 48 | p50: 10, p90: 15, p95: 20, p99: 25, max: 30, min: 5, num: 100 49 | }, 50 | disconnection_reason: "", 51 | call_analysis: { 52 | call_summary: "This was a mock call for testing purposes.", 53 | in_voicemail: false, 54 | user_sentiment: "neutral", 55 | call_successful: true, 56 | custom_analysis_data: {} 57 | } 58 | }; 59 | 60 | return NextResponse.json(mockCall); 61 | } 62 | -------------------------------------------------------------------------------- /app/api/retell/phone-numbers/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { RetellPhoneNumber } from '@/app/types/retell'; 3 | 4 | export async function GET() { 5 | const mockPhoneNumbers: RetellPhoneNumber[] = [ 6 | { 7 | phone_number: "+14155552671", 8 | phone_number_pretty: "(415) 555-2671", 9 | inbound_agent_id: "agent_123", 10 | outbound_agent_id: "agent_456", 11 | area_code: 415, 12 | nickname: "SF Office", 13 | last_modification_timestamp: Date.now() 14 | } 15 | ]; 16 | 17 | return NextResponse.json(mockPhoneNumbers); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/api/retell/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import Retell from 'retell-sdk'; 3 | 4 | const apiKey = process.env.RETELL_API_KEY; 5 | if (!apiKey) { 6 | throw new Error('RETELL_API_KEY is not defined in the environment variables'); 7 | } 8 | 9 | const client = new Retell({ 10 | apiKey, 11 | }); 12 | 13 | export async function GET(request: Request) { 14 | const { searchParams } = new URL(request.url); 15 | const callId = searchParams.get('callId'); 16 | 17 | if (!callId) { 18 | return NextResponse.json({ error: 'Call ID is required' }, { status: 400 }); 19 | } 20 | 21 | try { 22 | const callResponse = await client.call.retrieve(callId); 23 | return NextResponse.json(callResponse); 24 | } catch (error) { 25 | console.error('Error fetching call data:', error); 26 | return NextResponse.json({ error: 'Failed to fetch call data' }, { status: 500 }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/api/retell/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { Retell } from "retell-sdk" 3 | 4 | export async function POST(request: Request) { 5 | const body = await request.json() 6 | const signature = request.headers.get('x-retell-signature') as string 7 | 8 | if (!Retell.verify(JSON.stringify(body), process.env.RETELL_API_KEY!, signature)) { 9 | console.error("Invalid signature") 10 | return NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) 11 | } 12 | 13 | const { event, call } = body 14 | 15 | switch (event) { 16 | case "call_started": 17 | console.log("Call started event received", call.call_id) 18 | // Update call status in your database or state management 19 | break 20 | case "call_ended": 21 | console.log("Call ended event received", call.call_id) 22 | // Update call status and duration in your database or state management 23 | break 24 | case "call_analyzed": 25 | console.log("Call analyzed event received", call.call_id) 26 | // Update call summary in your database or state management 27 | break 28 | default: 29 | console.log("Received an unknown event:", event) 30 | } 31 | 32 | return NextResponse.json({ received: true }, { status: 200 }) 33 | } -------------------------------------------------------------------------------- /app/api/sales/products/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { Product } from "@/app/types/sales" 3 | 4 | const mockProducts: Product[] = [ 5 | { 6 | id: "1", 7 | name: "Premium Widget", 8 | description: "High-quality widget for all your needs", 9 | price: 99.99, // Utilisez des nombres à virgule flottante pour représenter les prix 10 | category: "Widgets", 11 | createdAt: "2023-01-15T10:00:00Z", 12 | updatedAt: "2023-01-15T10:00:00Z", 13 | }, 14 | { 15 | id: "2", 16 | name: "Super Gadget", 17 | description: "Next-generation gadget with advanced features", 18 | price: 149, 19 | category: "Gadgets", 20 | createdAt: "2023-02-20T14:30:00Z", 21 | updatedAt: "2023-02-20T14:30:00Z", 22 | }, 23 | { 24 | id: "3", 25 | name: "Eco-Friendly Gizmo", 26 | description: "Environmentally conscious gizmo for the modern home", 27 | price: 79, 28 | category: "Gizmos", 29 | createdAt: "2023-03-10T09:15:00Z", 30 | updatedAt: "2023-03-10T09:15:00Z", 31 | }, 32 | ] 33 | 34 | export async function GET() { 35 | return NextResponse.json(mockProducts) 36 | } 37 | 38 | export async function POST(request: Request) { 39 | const product: Product = await request.json() 40 | product.id = (mockProducts.length + 1).toString() 41 | product.createdAt = new Date().toISOString() 42 | product.updatedAt = new Date().toISOString() 43 | mockProducts.push(product) 44 | return NextResponse.json(product, { status: 201 }) 45 | } 46 | -------------------------------------------------------------------------------- /app/api/sales/proposals/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { Proposal } from "@/app/types/sales" 3 | 4 | const proposals: Proposal[] = [ 5 | { 6 | id: "1", 7 | title: "Website Redesign", 8 | client: "TechCorp", 9 | date: "2023-05-15", 10 | status: "sent", 11 | score: 85, 12 | createdAt: "2023-05-10T09:00:00Z", 13 | updatedAt: "2023-05-15T14:30:00Z" 14 | }, 15 | { 16 | id: "2", 17 | title: "Mobile App Development", 18 | client: "StartupX", 19 | date: "2023-06-01", 20 | status: "accepted", 21 | score: 92, 22 | createdAt: "2023-05-20T11:00:00Z", 23 | updatedAt: "2023-06-01T16:45:00Z" 24 | }, 25 | { 26 | id: "3", 27 | title: "Cloud Migration Strategy", 28 | client: "EnterpriseY", 29 | date: "2023-05-28", 30 | status: "draft", 31 | score: 78, 32 | createdAt: "2023-05-25T10:30:00Z", 33 | updatedAt: "2023-05-28T09:15:00Z" 34 | }, 35 | ] 36 | 37 | export async function GET() { 38 | return NextResponse.json(proposals) 39 | } 40 | 41 | export async function POST(request: Request) { 42 | const proposal: Proposal = await request.json() 43 | proposal.id = (proposals.length + 1).toString() 44 | proposal.createdAt = new Date().toISOString() 45 | proposal.updatedAt = new Date().toISOString() 46 | proposals.push(proposal) 47 | return NextResponse.json(proposal, { status: 201 }) 48 | } 49 | -------------------------------------------------------------------------------- /app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server" 2 | import { cookies } from "next/headers" 3 | import { NextRequest, NextResponse } from "next/server" 4 | import { generateEmbeddings } from "@/utils/embeddings" 5 | 6 | export const dynamic = 'force-dynamic' 7 | 8 | export async function GET(request: NextRequest) { 9 | try { 10 | const searchParams = request.nextUrl.searchParams 11 | const query = searchParams.get("q") 12 | const category = searchParams.get("category") 13 | const threshold = parseFloat(searchParams.get("threshold") || "0.7") 14 | 15 | if (!query) { 16 | return NextResponse.json({ error: "Query parameter is required" }, { status: 400 }) 17 | } 18 | 19 | const cookieStore = cookies() 20 | const supabase = createClient(cookieStore) 21 | 22 | // Générer l'embedding pour la requête 23 | const embedding = await generateEmbeddings(query) 24 | 25 | // Effectuer la recherche vectorielle 26 | let searchQuery = supabase.rpc("match_products", { 27 | query_embedding: embedding, 28 | match_threshold: threshold, 29 | match_count: 20 30 | }) 31 | 32 | // Appliquer des filtres supplémentaires si nécessaire 33 | if (category) { 34 | searchQuery = searchQuery.eq("category", category) 35 | } 36 | 37 | const { data: results, error } = await searchQuery 38 | 39 | if (error) throw error 40 | 41 | return NextResponse.json(results) 42 | } catch (error) { 43 | console.error("Search error:", error) 44 | return NextResponse.json( 45 | { error: "Failed to perform search" }, 46 | { status: 500 } 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/api/storage/contacts-data/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | const mockContactsData = [ 4 | { 5 | id: '1', 6 | name: 'John Doe', 7 | email: 'john.doe@example.com', 8 | phone: '+1234567890', 9 | lastInteraction: '2024-03-20' 10 | }, 11 | { 12 | id: '2', 13 | name: 'Jane Smith', 14 | email: 'jane.smith@example.com', 15 | phone: '+0987654321', 16 | lastInteraction: '2024-03-18' 17 | }, 18 | { 19 | id: '3', 20 | name: 'Alice Johnson', 21 | email: 'alice.johnson@example.com', 22 | phone: '+1122334455', 23 | lastInteraction: '2024-03-15' 24 | }, 25 | // Add more mock items as needed 26 | ] 27 | 28 | export async function GET() { 29 | // Simulate a delay to mimic a real API call 30 | await new Promise(resolve => setTimeout(resolve, 500)) 31 | 32 | return NextResponse.json(mockContactsData) 33 | } 34 | -------------------------------------------------------------------------------- /app/api/twilio/calls/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { TwilioCall } from '@/app/types/twilio' 3 | 4 | export async function GET() { 5 | const mockCalls: TwilioCall[] = [ 6 | { 7 | sid: 'CA1234567890abcdef1234567890abcdef', 8 | to: '+33123456789', 9 | from: '+33987654321', 10 | status: 'completed', 11 | startTime: '2023-06-01T10:00:00Z', 12 | endTime: '2023-06-01T10:05:00Z', 13 | duration: 300, 14 | direction: 'outbound-api', 15 | price: '0.50', 16 | }, 17 | // Add more mock calls here 18 | ] 19 | 20 | return NextResponse.json(mockCalls) 21 | } -------------------------------------------------------------------------------- /app/api/twilio/messages/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { TwilioMessage } from '@/app/types/twilio' 3 | 4 | export async function GET() { 5 | const mockMessages: TwilioMessage[] = [ 6 | { 7 | sid: 'SM1234567890abcdef1234567890abcdef', 8 | to: '+33123456789', 9 | from: '+33987654321', 10 | body: 'This is a test message', 11 | status: 'delivered', 12 | dateSent: '2023-06-01T11:00:00Z', 13 | direction: 'outbound-api', 14 | price: '0.10', 15 | }, 16 | // Add more mock messages here 17 | ] 18 | 19 | return NextResponse.json(mockMessages) 20 | } -------------------------------------------------------------------------------- /app/api/twilio/phone-numbers/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { TwilioPhoneNumber } from '@/app/types/twilio' 3 | 4 | export async function GET() { 5 | const mockPhoneNumbers: TwilioPhoneNumber[] = [ 6 | { 7 | sid: 'PN1234567890abcdef1234567890abcdef', 8 | phoneNumber: '+33123456789', 9 | friendlyName: 'Main Number', 10 | capabilities: { 11 | voice: true, 12 | SMS: true, 13 | MMS: true, 14 | }, 15 | }, 16 | // Add more mock phone numbers here 17 | ] 18 | 19 | return NextResponse.json(mockPhoneNumbers) 20 | } -------------------------------------------------------------------------------- /app/api/twilio/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST(request: Request) { 4 | const body = await request.text(); 5 | const params = new URLSearchParams(body); 6 | 7 | // Simuler la validation de la signature 8 | const isValid = true; 9 | 10 | if (!isValid) { 11 | return NextResponse.json({ error: 'Invalid signature' }, { status: 403 }); 12 | } 13 | 14 | // Simuler le traitement du webhook 15 | const messageStatus = params.get('MessageStatus'); 16 | const messageSid = params.get('MessageSid'); 17 | 18 | console.log(`Simulated webhook: Message ${messageSid} status updated to ${messageStatus}`); 19 | 20 | return NextResponse.json({ success: true }); 21 | } 22 | -------------------------------------------------------------------------------- /app/auth/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { cookies } from 'next/headers' 4 | import { createClient } from '@/utils/supabase/server' 5 | import { redirect } from 'next/navigation' 6 | 7 | export async function signIn(formData: FormData) { 8 | const cookieStore = cookies() 9 | const supabase = createClient(cookieStore) 10 | 11 | const { error } = await supabase.auth.signInWithPassword({ 12 | email: formData.get('email') as string, 13 | password: formData.get('password') as string, 14 | }) 15 | 16 | if (error) { 17 | return { error: error.message } 18 | } 19 | 20 | redirect('/dashboard') 21 | } 22 | 23 | export async function signUp(formData: FormData) { 24 | const cookieStore = cookies() 25 | const supabase = createClient(cookieStore) 26 | 27 | const { error } = await supabase.auth.signUp({ 28 | email: formData.get('email') as string, 29 | password: formData.get('password') as string, 30 | options: { 31 | emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, 32 | }, 33 | }) 34 | 35 | if (error) { 36 | return { error: error.message } 37 | } 38 | 39 | redirect('/verify-email') 40 | } 41 | 42 | export async function signOut() { 43 | const cookieStore = cookies() 44 | const supabase = createClient(cookieStore) 45 | await supabase.auth.signOut() 46 | redirect('/login') 47 | } 48 | -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/utils/supabase/server' 2 | import { NextResponse } from 'next/server' 3 | import { cookies } from 'next/headers' 4 | 5 | export async function GET(request: Request) { 6 | const requestUrl = new URL(request.url) 7 | const code = requestUrl.searchParams.get('code') 8 | 9 | if (code) { 10 | const cookieStore = cookies() 11 | const supabase = createClient(cookieStore) 12 | await supabase.auth.exchangeCodeForSession(code) 13 | } 14 | 15 | return NextResponse.redirect(new URL('/dashboard', request.url)) 16 | } 17 | -------------------------------------------------------------------------------- /app/dashboard/ai-agent/components/ai-agent-header.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { Settings, Plus } from "lucide-react" 3 | 4 | export function AIAgentHeader() { 5 | return ( 6 |
7 |
8 |

AI Assistant

9 |

10 | Your personal AI assistant for freelance work 11 |

12 |
13 |
14 | 17 | 21 |
22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /app/dashboard/ai-agent/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | export const metadata: Metadata = { 4 | title: "AI Agent - Dashboard", 5 | description: "AI Assistant for freelancers", 6 | } 7 | 8 | export default function AIAgentLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 |
15 | {children} 16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /app/dashboard/ai-agent/page.tsx: -------------------------------------------------------------------------------- 1 | import { AIAgentChat } from "./components/ai-agent-chat" 2 | import { AIAgentHeader } from "./components/ai-agent-header" 3 | 4 | export default function AIAgentPage() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /app/dashboard/clients/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | // app/dashboard/clients/[id]/page.tsx 2 | import { ClientProfileComponent } from "@/components/client-profile/client-profile" 3 | import { createClient } from "@/utils/supabase/server" 4 | import { cookies } from "next/headers" 5 | import { notFound } from "next/navigation" 6 | 7 | export default async function ClientProfilePage({ 8 | params 9 | }: { 10 | params: { id: string } 11 | }) { 12 | const cookieStore = cookies() 13 | const supabase = createClient(cookieStore) 14 | 15 | const { data: client } = await supabase 16 | .from('contacts') 17 | .select('*') 18 | .eq('id', params.id) 19 | .single() 20 | 21 | if (!client) { 22 | notFound() 23 | } 24 | 25 | return 26 | } -------------------------------------------------------------------------------- /app/dashboard/contacts/add-contact-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" 2 | import { AddContactForm } from "./add-contact-form" 3 | import { Contact } from "@/app/types/contact" 4 | 5 | interface AddContactDialogProps { 6 | isOpen: boolean 7 | onClose: () => void 8 | onAddContact: (data: Partial) => Promise 9 | } 10 | 11 | export function AddContactDialog({ isOpen, onClose, onAddContact }: AddContactDialogProps) { 12 | return ( 13 | 14 | 15 | 16 | Add New Contact 17 | 18 | 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /app/dashboard/contacts/edit-contact-dialog.tsx: -------------------------------------------------------------------------------- 1 | // edit-contact-dialog.tsx 2 | import { type Contact } from "@/app/types/contact" 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog" 9 | import { AddContactForm } from "./add-contact-form" 10 | 11 | export function EditContactDialog({ 12 | contact, 13 | isOpen, 14 | onClose, 15 | onEdit 16 | }: { 17 | contact: Contact | null 18 | isOpen: boolean 19 | onClose: () => void 20 | onEdit: (data: Partial) => Promise 21 | }) { 22 | // Transform contact data to match form structure 23 | const initialData = contact ? { 24 | ...contact, 25 | provider: { 26 | site: contact.provider?.site || "", 27 | funnel: contact.provider?.funnel || "", 28 | call: contact.provider?.call || "", 29 | vip: contact.provider?.vip || false, 30 | }, 31 | tags: contact.tags || [], 32 | score: contact.score || 0, 33 | } : null 34 | 35 | return ( 36 | 37 | 38 | 39 | Modifier le contact 40 | 41 | 46 | 47 | 48 | ) 49 | } -------------------------------------------------------------------------------- /app/dashboard/contacts/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { FileX } from "lucide-react" 2 | 3 | export function EmptyState() { 4 | return ( 5 |
6 | 7 |

No contacts found

8 |

Try adjusting your search or filter to find what you're looking for.

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/dashboard/contacts/error-state.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircle } from "lucide-react" 2 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 3 | import { Button } from "@/components/ui/button" 4 | 5 | interface ErrorStateProps { 6 | message: string 7 | onRetry: () => void 8 | } 9 | 10 | export function ErrorState({ message, onRetry }: ErrorStateProps) { 11 | return ( 12 | 13 | 14 | Error 15 | 16 | {message} 17 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/dashboard/contacts/import-contact-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" 2 | import { ImportContactForm } from "./import-contact-form" 3 | import { Contact } from "@/app/types/contact" 4 | import { Button } from "@/components/ui/button" 5 | import { Download } from "lucide-react" 6 | 7 | interface ImportContactDialogProps { 8 | isOpen: boolean 9 | onClose: () => void 10 | onImport: (contacts: Partial[]) => Promise 11 | } 12 | 13 | export function ImportContactDialog({ isOpen, onClose, onImport }: ImportContactDialogProps) { 14 | const downloadExample = () => { 15 | const element = document.createElement("a") 16 | element.href = "/example-contact.csv" 17 | element.download = "example-contact.csv" 18 | document.body.appendChild(element) 19 | element.click() 20 | document.body.removeChild(element) 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | Import Contacts 28 | 37 | 38 | 39 | 40 | 41 | ) 42 | } -------------------------------------------------------------------------------- /app/dashboard/contacts/loading-state.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from "@/components/ui/table" 10 | 11 | export function LoadingState() { 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 | {Array.from({ length: 7 }).map((_, index) => ( 22 | 23 | 24 | 25 | ))} 26 | 27 | 28 | 29 | {Array.from({ length: 5 }).map((_, rowIndex) => ( 30 | 31 | {Array.from({ length: 7 }).map((_, cellIndex) => ( 32 | 33 | 34 | 35 | ))} 36 | 37 | ))} 38 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /app/dashboard/contacts/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ContactsClient } from "./contacts-client" 4 | 5 | export default function ContactsPage() { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/dashboard/contacts/tag-filter.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { MultiSelect } from "@/components/ui/multi-select" 3 | import { useQuery } from '@tanstack/react-query' 4 | 5 | interface Tag { 6 | id: string 7 | name: string 8 | } 9 | 10 | interface TagFilterProps { 11 | value: string[] 12 | onChange: (value: string[]) => void 13 | } 14 | 15 | export const TagFilter: FC = ({ value, onChange }) => { 16 | const { data: tags = [] } = useQuery({ 17 | queryKey: ['tags'], 18 | queryFn: async () => { 19 | const response = await fetch('/api/contacts/tags') 20 | const data = await response.json() 21 | return data.data 22 | } 23 | }) 24 | 25 | return ( 26 | ({ 30 | label: tag.name, 31 | value: tag.id, 32 | }))} 33 | placeholder="Filter by tags..." 34 | /> 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/dashboard/contacts/toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { Search, Upload, UserPlus } from "lucide-react" 3 | import { Contact } from "@/app/types/contact" 4 | import { Filters } from "@/app/types/filters" 5 | import { Table } from "@tanstack/react-table" 6 | import { Button } from "@/components/ui/button" 7 | import { Input } from "@/components/ui/input" 8 | import { AddContactDialog } from "./add-contact-dialog" 9 | import { ImportContactDialog } from "./import-contact-dialog" 10 | 11 | interface ToolbarProps { 12 | table: Table 13 | onAddContact: (data: Partial) => Promise 14 | onImportContact: (contacts: Partial[]) => Promise 15 | filters: Filters 16 | onFiltersChange: (filters: Filters) => void 17 | } 18 | 19 | export function Toolbar({ 20 | onAddContact, 21 | onImportContact, 22 | filters, 23 | onFiltersChange, 24 | }: Omit) { 25 | const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) 26 | const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | 34 | onFiltersChange({ ...filters, search: e.target.value })} 38 | className="pl-9 pr-4 py-2 w-full rounded-full" 39 | /> 40 |
41 |
42 |
43 | 50 | 51 | 59 |
60 |
61 | 62 | setIsAddDialogOpen(false)} 65 | onAddContact={onAddContact} 66 | /> 67 | 68 | setIsImportDialogOpen(false)} 71 | onImport={onImportContact} 72 | /> 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { cookies } from 'next/headers' 4 | import { createClient } from '@/utils/supabase/server' 5 | import { AppSidebarWrapper } from "@/components/app-sidebar/app-sidebar-wrapper" 6 | import { redirect } from 'next/navigation' 7 | import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar" 8 | 9 | export default async function DashboardLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | const cookieStore = cookies() 15 | const supabase = createClient(cookieStore) 16 | 17 | const { data: { user }, error } = await supabase.auth.getUser() 18 | 19 | if (error || !user) { 20 | redirect('/login') 21 | } 22 | 23 | const userMetadata = { 24 | avatar_url: user.user_metadata?.avatar_url, 25 | full_name: user.user_metadata?.full_name 26 | } 27 | 28 | return ( 29 | 30 | 31 | 32 |
33 | 34 |

Dashboard

35 |
36 |
37 | {children} 38 |
39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/dashboard/messages/email/page.tsx: -------------------------------------------------------------------------------- 1 | import { EmailDashboard } from "@/components/email/email" 2 | 3 | export default function EmailPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { cookies } from 'next/headers' 3 | import { createClient } from '@/utils/supabase/server' 4 | import { redirect } from 'next/navigation' 5 | 6 | export const metadata: Metadata = { 7 | title: "Dashboard", 8 | description: "Example dashboard app built using the components.", 9 | } 10 | 11 | export default async function DashboardPage() { 12 | const cookieStore = cookies() 13 | const supabase = createClient(cookieStore) 14 | 15 | // Toujours utiliser getUser() pour la validation 16 | const { data: { user }, error } = await supabase.auth.getUser() 17 | 18 | if (error || !user) { 19 | redirect('/login') 20 | } 21 | 22 | return ( 23 |
24 |
25 |

Dashboard

26 |
27 | {/* Add your dashboard content here */} 28 |
29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /app/dashboard/products/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | import { createClient } from "@/utils/supabase/server" 3 | import { cookies } from "next/headers" 4 | import { ProductForm } from "../../product-form" 5 | import { ProductFormValues, DatabaseProduct, transformProductFromDb } from "@/app/types/product" 6 | import { updateProduct } from "../../actions" 7 | 8 | export default async function EditProductPage({ params }: { params: { id: string } }) { 9 | const cookieStore = cookies() 10 | const supabase = createClient(cookieStore) 11 | 12 | const { data: dbProduct, error } = await supabase 13 | .from("products") 14 | .select("*") 15 | .eq("id", params.id) 16 | .single() 17 | 18 | if (error || !dbProduct) { 19 | notFound() 20 | } 21 | 22 | const product = transformProductFromDb(dbProduct) 23 | 24 | async function handleUpdate(values: ProductFormValues) { 25 | 'use server' 26 | return updateProduct(params.id, values) 27 | } 28 | 29 | const formInitialData: ProductFormValues = { 30 | title: product.title, 31 | description: product.description, 32 | type: product.type, 33 | category: product.category, 34 | subcategory: product.subcategory, 35 | price: product.price, 36 | technicalSpecs: product.technicalSpecs, 37 | tags: product.tags, 38 | media: { 39 | images: product.media.images, 40 | videos: product.media.videos || [], 41 | documents: product.media.documents || [] 42 | }, 43 | metadata: { 44 | duration: product.metadata.duration || "", 45 | efficiency: product.metadata.efficiency || "", 46 | warranty: product.metadata.warranty || "", 47 | maintenance: product.metadata.maintenance || "", 48 | certifications: product.metadata.certifications || [] 49 | } 50 | } 51 | 52 | return ( 53 |
54 |
55 |

Modifier le Produit

56 |

57 | Modifiez les informations du produit 58 |

59 |
60 | 64 |
65 | ) 66 | } -------------------------------------------------------------------------------- /app/dashboard/products/delete-product-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | AlertDialog, 5 | AlertDialogAction, 6 | AlertDialogCancel, 7 | AlertDialogContent, 8 | AlertDialogDescription, 9 | AlertDialogFooter, 10 | AlertDialogHeader, 11 | AlertDialogTitle, 12 | } from "@/components/ui/alert-dialog" 13 | import { useToast } from "@/hooks/use-toast" 14 | import { deleteProduct } from "@/app/actions/products" 15 | 16 | interface DeleteProductDialogProps { 17 | productId: string 18 | isOpen: boolean 19 | onClose: () => void 20 | } 21 | 22 | export function DeleteProductDialog({ 23 | productId, 24 | isOpen, 25 | onClose, 26 | }: DeleteProductDialogProps) { 27 | const { toast } = useToast() 28 | 29 | async function handleDelete() { 30 | try { 31 | await deleteProduct(productId) 32 | toast({ 33 | title: "Produit supprimé", 34 | description: "Le produit a été supprimé avec succès", 35 | }) 36 | onClose() 37 | } catch (error) { 38 | console.error("Erreur lors de la suppression:", error) 39 | toast({ 40 | variant: "destructive", 41 | title: "Erreur", 42 | description: error instanceof Error 43 | ? error.message 44 | : "Une erreur est survenue lors de la suppression", 45 | }) 46 | } 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | Êtes-vous sûr ? 54 | 55 | Cette action est irréversible. Le produit sera définitivement supprimé 56 | de votre catalogue. 57 | 58 | 59 | 60 | Annuler 61 | 65 | Supprimer 66 | 67 | 68 | 69 | 70 | ) 71 | } -------------------------------------------------------------------------------- /app/dashboard/products/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { ProductForm } from "../product-form" 2 | import { createProduct } from "../actions" 3 | import { ProductFormValues } from "@/app/types/product" 4 | 5 | export default function NewProductPage() { 6 | async function handleCreate(values: ProductFormValues) { 7 | 'use server' 8 | return createProduct(values) 9 | } 10 | 11 | return ( 12 |
13 |
14 |

Nouveau Produit

15 |

16 | Créez un nouveau produit ou service dans votre catalogue 17 |

18 |
19 | 22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /app/dashboard/products/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { ProductsList } from "./products-list" 5 | import { ProductsHeader } from "./products-header" 6 | import { AdvancedSearch } from "./advanced-search" 7 | import { useSearch } from "@/hooks/use-search" 8 | import { useProducts } from "@/hooks/use-products" 9 | import { Product } from "@/app/types/product" 10 | 11 | export default function ProductsPage() { 12 | const { results, isSearching } = useSearch() 13 | const { data: products, isLoading } = useProducts() 14 | const [isSearchMode, setIsSearchMode] = useState(false) 15 | 16 | const handleSearchResults = (searchResults: Product[]) => { 17 | setIsSearchMode(searchResults.length > 0) 18 | } 19 | 20 | return ( 21 |
22 | 23 | 24 | 28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /app/dashboard/products/product-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Product } from "@/app/types/product" 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card" 11 | import { Badge } from "@/components/ui/badge" 12 | import { formatPrice } from "@/lib/utils" 13 | import { Button } from "@/components/ui/button" 14 | import { Edit, Trash } from "lucide-react" 15 | import { useRouter } from "next/navigation" 16 | 17 | interface ProductCardProps { 18 | product: Product 19 | } 20 | 21 | export function ProductCard({ product }: ProductCardProps) { 22 | const router = useRouter() 23 | 24 | const renderPrice = () => { 25 | if (product.price.hasRange && product.price.range) { 26 | return `${formatPrice(product.price.range.min || 0)} - ${formatPrice(product.price.range.max || 0)}` 27 | } 28 | return formatPrice(product.price.base) 29 | } 30 | 31 | return ( 32 | 33 | 34 |
35 |
36 | {product.title} 37 | {product.type} 38 |
39 | {product.category} 40 |
41 |
42 | 43 |
44 |

45 | {product.description} 46 |

47 |
48 |
49 | {renderPrice()} 50 |
51 |
52 | 59 | 62 |
63 |
64 |
65 |
66 |
67 | ) 68 | } -------------------------------------------------------------------------------- /app/dashboard/products/products-header.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { useRouter } from "next/navigation" 5 | import { Plus } from "lucide-react" 6 | 7 | export function ProductsHeader() { 8 | const router = useRouter() 9 | 10 | return ( 11 |
12 |
13 |

Produits & Services

14 |

15 | Gérez votre catalogue de produits et services 16 |

17 |
18 | 25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /app/dashboard/products/products-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Product } from "@/app/types/product" 4 | import { ProductCard } from "./product-card" 5 | import { Alert, AlertDescription } from "@/components/ui/alert" 6 | import { Loader2 } from "lucide-react" 7 | 8 | interface ProductsListProps { 9 | products?: Product[]; 10 | isLoading?: boolean; 11 | } 12 | 13 | export function ProductsList({ products, isLoading }: ProductsListProps) { 14 | if (isLoading) { 15 | return ( 16 |
17 | 18 |
19 | ) 20 | } 21 | 22 | if (!products || products.length === 0) { 23 | return ( 24 | 25 | 26 | Aucun produit trouvé. Commencez par en créer un ! 27 | 28 | 29 | ) 30 | } 31 | 32 | return ( 33 |
34 | {products.map((product: Product) => ( 35 | 36 | ))} 37 |
38 | ) 39 | } -------------------------------------------------------------------------------- /app/dashboard/products/products-search.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Input } from "@/components/ui/input" 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select" 11 | import { useRouter, useSearchParams } from "next/navigation" 12 | import { useCallback, useTransition } from "react" 13 | import debounce from "lodash/debounce" 14 | import { Loader2 } from "lucide-react" 15 | 16 | export function ProductsSearch() { 17 | const router = useRouter() 18 | const searchParams = useSearchParams() 19 | const [isPending, startTransition] = useTransition() 20 | 21 | const handleSearch = useCallback((term: string) => { 22 | const params = new URLSearchParams(searchParams) 23 | if (term) { 24 | params.set("query", term) 25 | } else { 26 | params.delete("query") 27 | } 28 | startTransition(() => { 29 | router.push(`/dashboard/products?${params.toString()}`) 30 | }) 31 | }, [router, searchParams]) 32 | 33 | const debouncedSearch = debounce(handleSearch, 300) 34 | 35 | const handleCategoryChange = (category: string) => { 36 | const params = new URLSearchParams(searchParams) 37 | if (category) { 38 | params.set("category", category) 39 | } else { 40 | params.delete("category") 41 | } 42 | startTransition(() => { 43 | router.push(`/dashboard/products?${params.toString()}`) 44 | }) 45 | } 46 | 47 | return ( 48 |
49 |
50 | debouncedSearch(e.target.value)} 53 | className="pr-8" 54 | /> 55 | {isPending && ( 56 |
57 | 58 |
59 | )} 60 |
61 | 76 |
77 | ) 78 | } -------------------------------------------------------------------------------- /app/dashboard/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers' 2 | import { createClient } from '@/utils/supabase/server' 3 | import { redirect } from 'next/navigation' 4 | 5 | export default async function ProfilePage() { 6 | const cookieStore = cookies() 7 | const supabase = createClient(cookieStore) 8 | 9 | const { data: { user }, error } = await supabase.auth.getUser() 10 | 11 | if (error || !user) { 12 | redirect('/login') 13 | } 14 | 15 | return ( 16 |
17 |

Profile Settings

18 | {/* Ajoutez ici votre formulaire de profil */} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/dashboard/retell/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Overview from "@/components/retell/Overview" 4 | 5 | export default function RetellPage() { 6 | return ( 7 |
8 |

Retell Dashboard

9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/dashboard/sales/analysis/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 5 | import { DataTable } from "@/components/ui/data-table" 6 | import { Badge } from "@/components/ui/badge" 7 | import { Proposal } from "@/app/types/proposal" 8 | import { ColumnDef } from "@tanstack/react-table" 9 | import { useProposals } from "@/hooks/use-proposals" 10 | 11 | const columns: ColumnDef[] = [ 12 | { accessorKey: "title", header: "Title" }, 13 | { accessorKey: "client", header: "Client" }, 14 | { accessorKey: "date", header: "Date" }, 15 | { 16 | accessorKey: "status", 17 | header: "Status", 18 | cell: ({ row }) => ( 19 | 25 | {row.original.status} 26 | 27 | ) 28 | }, 29 | { 30 | accessorKey: "score", 31 | header: "Score", 32 | cell: ({ row }) => { 33 | const score = row.original.score 34 | if (!score) return - 35 | 36 | return ( 37 | = 70 ? "success" : "warning"}> 38 | {score} 39 | 40 | ) 41 | }, 42 | }, 43 | ] 44 | 45 | export default function AnalysisPage() { 46 | const { 47 | proposals, 48 | isLoading, 49 | error 50 | } = useProposals() 51 | const [selectedProposal, setSelectedProposal] = useState(null) 52 | 53 | if (isLoading) return
Loading...
54 | if (error) return
Error: {error.message}
55 | 56 | const handleView = (proposal: Proposal) => { 57 | setSelectedProposal(proposal) 58 | console.log("Viewing proposal:", proposal) 59 | } 60 | 61 | return ( 62 |
63 |

Proposal Analysis

64 | 65 | 66 | Proposal Performance 67 | 68 | 69 | 74 | 75 | 76 | {selectedProposal && ( 77 |
78 |

Selected Proposal Details

79 |
{JSON.stringify(selectedProposal, null, 2)}
80 |
81 | )} 82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /app/dashboard/sales/page.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 2 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 3 | import ProductsPage from "./products/page" 4 | import ProposalPage from "./proposals/page" 5 | import AnalysisPage from "./analysis/page" 6 | 7 | export default function SalesPage() { 8 | return ( 9 |
10 |

Sales Dashboard

11 | 12 | 13 | Products 14 | Proposal 15 | Analysis 16 | 17 | 18 | 19 | 20 | Products 21 | Manage your product catalog 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Proposal 32 | Create and manage proposals 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Analysis 43 | Analyze your sales performance 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/dashboard/sales/products/product-form.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { Product } from "@/app/types/sales" 3 | import { Button } from "@/components/ui/button" 4 | import { Input } from "@/components/ui/input" 5 | import { Textarea } from "@/components/ui/textarea" 6 | 7 | interface ProductFormProps { 8 | product: Product | null 9 | onSave: () => void 10 | } 11 | 12 | export function ProductForm({ product, onSave }: ProductFormProps) { 13 | const [formData, setFormData] = useState>(product || {}) 14 | 15 | const handleChange = (e: React.ChangeEvent) => { 16 | const { name, value } = e.target 17 | setFormData(prev => ({ ...prev, [name]: value })) 18 | } 19 | 20 | const handleSubmit = (e: React.FormEvent) => { 21 | e.preventDefault() 22 | // Ici, vous devriez appeler votre API pour sauvegarder le produit 23 | console.log("Saving product:", formData) 24 | onSave() 25 | } 26 | 27 | return ( 28 |
29 | 35 |