diff --git a/doc/prompts/9. Add contact recored b/doc/prompts/9. Add contact recored new file mode 100644 index 0000000..a34071d --- /dev/null +++ b/doc/prompts/9. Add contact recored @@ -0,0 +1,13 @@ +name new branch features/customer_recored + +Now I want to add Concact Recored to. Contact Record is the history I send emails to each customer. + +Tablename: ContactRecord +id, customerId, contactType, notes, modifiedAt, createdAt + +UIChanges +Add CRUD operation for ContactRecored. +Show id to customer list. +Add Cusotomer detail page to Customer model. Users can go customer detail page by click id or customer name in customer list. + +Commit all changes to features/customer_record \ No newline at end of file diff --git a/src/app/(admin)/admin/contact-records/components/EditContactRecord.tsx b/src/app/(admin)/admin/contact-records/components/EditContactRecord.tsx new file mode 100644 index 0000000..8a77a39 --- /dev/null +++ b/src/app/(admin)/admin/contact-records/components/EditContactRecord.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { useState, useEffect, FormEvent, ChangeEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface ContactRecord { + id: string; + customerId: string; + contactType: string; + notes: string; + createdAt: string; + modifiedAt: string; + customer?: { + id: string; + name: string; + }; +} + +interface EditContactRecordProps { + id: string; +} + +export default function EditContactRecord({ id }: EditContactRecordProps) { + const router = useRouter(); + const [formData, setFormData] = useState({ + contactType: '', + notes: '', + }); + const [customerId, setCustomerId] = useState(''); + const [customerName, setCustomerName] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Fetch contact record data + useEffect(() => { + const fetchContactRecord = async () => { + try { + const response = await fetch(`/api/contact-records/${id}`); + + if (!response.ok) { + throw new Error('Failed to fetch contact record'); + } + + const contactRecord: ContactRecord = await response.json(); + + setFormData({ + contactType: contactRecord.contactType, + notes: contactRecord.notes || '', + }); + + setCustomerId(contactRecord.customerId); + + // If customer relation is loaded + if (contactRecord.customer) { + setCustomerName(contactRecord.customer.name); + } else { + // Fetch customer name separately if not included in the contact record + const customerResponse = await fetch(`/api/customers/${contactRecord.customerId}`); + if (customerResponse.ok) { + const customer = await customerResponse.json(); + setCustomerName(customer.name); + } + } + + setIsLoading(false); + } catch (err) { + setError('Failed to load contact record data'); + setIsLoading(false); + } + }; + + fetchContactRecord(); + }, [id]); + + const handleChange = ( + e: ChangeEvent + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + // Validate form + if (!formData.contactType) { + throw new Error('Contact type is required'); + } + + // Submit the form + const response = await fetch(`/api/contact-records/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + contactType: formData.contactType, + notes: formData.notes, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to update contact record'); + } + + // Redirect back to customer detail page + router.push(`/admin/customers/detail/${customerId}`); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + setIsSubmitting(false); + } + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+

Edit Contact Record

+ + Back to Customer + +
+ + {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} + +
+
+ +
+ {customerName} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + + Cancel + +
+
+
+
+ ); +} diff --git a/src/app/(admin)/admin/contact-records/edit/[id]/page.tsx b/src/app/(admin)/admin/contact-records/edit/[id]/page.tsx new file mode 100644 index 0000000..ff7dbfc --- /dev/null +++ b/src/app/(admin)/admin/contact-records/edit/[id]/page.tsx @@ -0,0 +1,6 @@ +import EditContactRecord from '../../components/EditContactRecord'; + +export default async function EditContactRecordPage({ params }: { params: { id: string } }) { + const { id } = params; + return ; +} diff --git a/src/app/(admin)/admin/customers/components/ContactRecordList.tsx b/src/app/(admin)/admin/customers/components/ContactRecordList.tsx new file mode 100644 index 0000000..bc38894 --- /dev/null +++ b/src/app/(admin)/admin/customers/components/ContactRecordList.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface ContactRecord { + id: string; + customerId: string; + contactType: string; + notes: string; + createdAt: string; + modifiedAt: string; +} + +interface ContactRecordListProps { + contactRecords: ContactRecord[]; +} + +export default function ContactRecordList({ contactRecords }: ContactRecordListProps) { + const router = useRouter(); + const [isDeleting, setIsDeleting] = useState(null); + const [error, setError] = useState(null); + + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this contact record?')) { + return; + } + + setIsDeleting(id); + setError(null); + + try { + const response = await fetch(`/api/contact-records/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete contact record'); + } + + // Refresh the page to show updated contact record list + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsDeleting(null); + } + }; + + if (contactRecords.length === 0) { + return ( +
+ No contact records found. Add your first contact record above. +
+ ); + } + + return ( +
+ {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} + +
    + {contactRecords.map((record) => ( +
  • +
    +
    +
    + {record.contactType} + + {new Date(record.createdAt).toLocaleString()} + +
    +
    + {record.notes || No notes} +
    +
    +
    + + Edit + + +
    +
    +
  • + ))} +
+
+ ); +} diff --git a/src/app/(admin)/admin/customers/components/NewContactRecordForm.tsx b/src/app/(admin)/admin/customers/components/NewContactRecordForm.tsx new file mode 100644 index 0000000..4344775 --- /dev/null +++ b/src/app/(admin)/admin/customers/components/NewContactRecordForm.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useState, FormEvent, ChangeEvent } from 'react'; +import { useRouter } from 'next/navigation'; + +interface NewContactRecordFormProps { + customerId: string; +} + +export default function NewContactRecordForm({ customerId }: NewContactRecordFormProps) { + const router = useRouter(); + const [formData, setFormData] = useState({ + contactType: '', + notes: '', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleChange = ( + e: ChangeEvent + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + setSuccess(false); + + try { + // Validate form + if (!formData.contactType) { + throw new Error('Contact type is required'); + } + + // Submit the form + const response = await fetch('/api/contact-records', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + customerId, + contactType: formData.contactType, + notes: formData.notes, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to create contact record'); + } + + // Reset form on success + setFormData({ + contactType: '', + notes: '', + }); + setSuccess(true); + + // Refresh the page to show the new contact record + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} + + {success && ( +
+
+
+ + + +
+
+

Contact record created successfully!

+
+
+
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ ); +} diff --git a/src/app/(admin)/admin/customers/detail/[id]/page.tsx b/src/app/(admin)/admin/customers/detail/[id]/page.tsx new file mode 100644 index 0000000..a89c442 --- /dev/null +++ b/src/app/(admin)/admin/customers/detail/[id]/page.tsx @@ -0,0 +1,138 @@ +import Link from 'next/link'; +import { getDataSource, Customer, ContactRecord } from '@/lib/database'; +import ContactRecordList from '../../components/ContactRecordList'; +import NewContactRecordForm from '../../components/NewContactRecordForm'; + +export default async function CustomerDetail({ params }: { params: { id: string } }) { + const { id } = params; + + // Fetch customer data + const dataSource = await getDataSource(); + const customerRepository = dataSource.getRepository(Customer); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + + const customer = await customerRepository.findOne({ + where: { id } + }); + + if (!customer) { + return ( +
+

Customer Not Found

+

The customer you're looking for doesn't exist or has been deleted.

+ + Back to Customers + +
+ ); + } + + // Fetch contact records for this customer + const contactRecords = await contactRecordRepository.find({ + where: { customerId: id }, + order: { createdAt: 'DESC' } + }); + + return ( +
+
+

Customer Details

+
+ + Edit Customer + + + Back to Customers + +
+
+ + {/* Customer Information Card */} +
+
+

Customer Information

+

Details about the customer.

+
+
+
+
+
ID
+
{customer.id}
+
+
+
Name
+
{customer.name}
+
+
+
Email
+
+ + {customer.email} + +
+
+
+
Website
+
+ {customer.url ? ( + + {customer.url} + + ) : ( + - + )} +
+
+
+
Created
+
+ {new Date(customer.createdAt).toLocaleString()} +
+
+
+
Last Modified
+
+ {new Date(customer.modifiedAt).toLocaleString()} +
+
+
+
+
+ + {/* Contact Records Section */} +
+
+
+

Contact Records

+

History of communications with this customer.

+
+
+ + {/* New Contact Record Form */} +
+

Add New Contact Record

+ +
+ + {/* Contact Records List */} +
+ +
+
+
+ ); +} diff --git a/src/app/(admin)/admin/customers/page.tsx b/src/app/(admin)/admin/customers/page.tsx index b782d64..dbec913 100644 --- a/src/app/(admin)/admin/customers/page.tsx +++ b/src/app/(admin)/admin/customers/page.tsx @@ -33,6 +33,12 @@ export default async function AdminCustomers() { + ID + + Name @@ -70,7 +76,20 @@ export default async function AdminCustomers() { customers.map((customer) => ( - {customer.name} + + {customer.id.substring(0, 8)}... + + + + + {customer.name} + {customer.url ? ( @@ -113,7 +132,7 @@ export default async function AdminCustomers() { )) ) : ( - + No customers found. Create your first customer! diff --git a/src/app/api/contact-records/[id]/route.ts b/src/app/api/contact-records/[id]/route.ts new file mode 100644 index 0000000..3567838 --- /dev/null +++ b/src/app/api/contact-records/[id]/route.ts @@ -0,0 +1,136 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDataSource, ContactRecord, Customer } from '@/lib/database'; + +// GET /api/contact-records/[id] - Get a specific contact record +export async function GET( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + + const contactRecord = await contactRecordRepository.findOne({ + where: { id: id }, + relations: ['customer'] + }); + + if (!contactRecord) { + return NextResponse.json( + { error: 'Contact record not found' }, + { status: 404 } + ); + } + + return NextResponse.json(contactRecord); + } catch (error) { + console.error('Error fetching contact record:', error); + return NextResponse.json( + { error: 'Failed to fetch contact record' }, + { status: 500 } + ); + } +} + +// PUT /api/contact-records/[id] - Update a contact record +export async function PUT( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + const customerRepository = dataSource.getRepository(Customer); + + // Find the contact record to update + const contactRecord = await contactRecordRepository.findOne({ + where: { id: id } + }); + + if (!contactRecord) { + return NextResponse.json( + { error: 'Contact record not found' }, + { status: 404 } + ); + } + + const data = await request.json(); + const { customerId, contactType, notes } = data; + + // Validate required fields + if (!contactType) { + return NextResponse.json( + { error: 'Contact type is required' }, + { status: 400 } + ); + } + + // If customerId is changing, verify the new customer exists + if (customerId && customerId !== contactRecord.customerId) { + const customer = await customerRepository.findOne({ + where: { id: customerId } + }); + + if (!customer) { + return NextResponse.json( + { error: 'Customer not found' }, + { status: 404 } + ); + } + + contactRecord.customerId = customerId; + } + + // Update contact record fields + contactRecord.contactType = contactType; + contactRecord.notes = notes || ''; + + // Save the updated contact record + const updatedContactRecord = await contactRecordRepository.save(contactRecord); + + return NextResponse.json(updatedContactRecord); + } catch (error) { + console.error('Error updating contact record:', error); + return NextResponse.json( + { error: 'Failed to update contact record' }, + { status: 500 } + ); + } +} + +// DELETE /api/contact-records/[id] - Delete a contact record +export async function DELETE( + request: NextRequest, + props: { params: Promise<{ id: string }> } +) { + try { + const { id } = await props.params; + const dataSource = await getDataSource(); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + + // Find the contact record to delete + const contactRecord = await contactRecordRepository.findOne({ + where: { id: id } + }); + + if (!contactRecord) { + return NextResponse.json( + { error: 'Contact record not found' }, + { status: 404 } + ); + } + + // Delete the contact record + await contactRecordRepository.remove(contactRecord); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting contact record:', error); + return NextResponse.json( + { error: 'Failed to delete contact record' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/contact-records/route.ts b/src/app/api/contact-records/route.ts new file mode 100644 index 0000000..c6b2f28 --- /dev/null +++ b/src/app/api/contact-records/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDataSource, ContactRecord, Customer } from '@/lib/database'; + +// GET /api/contact-records - Get all contact records +export async function GET(request: NextRequest) { + try { + const dataSource = await getDataSource(); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + + // Get query parameters + const url = new URL(request.url); + const customerId = url.searchParams.get('customerId'); + + // Build query + const queryOptions: any = { + order: { createdAt: 'DESC' }, + relations: ['customer'] + }; + + // Filter by customer if customerId is provided + if (customerId) { + queryOptions.where = { customerId }; + } + + const contactRecords = await contactRecordRepository.find(queryOptions); + + return NextResponse.json(contactRecords); + } catch (error) { + console.error('Error fetching contact records:', error); + return NextResponse.json( + { error: 'Failed to fetch contact records' }, + { status: 500 } + ); + } +} + +// POST /api/contact-records - Create a new contact record +export async function POST(request: NextRequest) { + try { + const dataSource = await getDataSource(); + const contactRecordRepository = dataSource.getRepository(ContactRecord); + const customerRepository = dataSource.getRepository(Customer); + + const data = await request.json(); + const { customerId, contactType, notes } = data; + + // Validate required fields + if (!customerId || !contactType) { + return NextResponse.json( + { error: 'Customer ID and contact type are required' }, + { status: 400 } + ); + } + + // Verify customer exists + const customer = await customerRepository.findOne({ + where: { id: customerId } + }); + + if (!customer) { + return NextResponse.json( + { error: 'Customer not found' }, + { status: 404 } + ); + } + + // Create and save the new contact record + const contactRecord = new ContactRecord(); + contactRecord.customerId = customerId; + contactRecord.contactType = contactType; + contactRecord.notes = notes || ''; + + const savedContactRecord = await contactRecordRepository.save(contactRecord); + + return NextResponse.json(savedContactRecord, { status: 201 }); + } catch (error) { + console.error('Error creating contact record:', error); + return NextResponse.json( + { error: 'Failed to create contact record' }, + { status: 500 } + ); + } +} diff --git a/src/lib/database/config.ts b/src/lib/database/config.ts index 8d9d649..52b3191 100644 --- a/src/lib/database/config.ts +++ b/src/lib/database/config.ts @@ -2,13 +2,14 @@ import { DataSource, DataSourceOptions } from 'typeorm'; import { User } from './entities/User'; import { Post } from './entities/Post'; import { Customer } from './entities/Customer'; +import { ContactRecord } from './entities/ContactRecord'; import path from 'path'; // Default configuration for SQLite (development/testing) const sqliteConfig: DataSourceOptions = { type: 'sqlite', database: path.join(process.cwd(), 'data', 'database.sqlite'), - entities: [User, Post, Customer], + entities: [User, Post, Customer, ContactRecord], synchronize: true, // Set to false in production logging: process.env.NODE_ENV === 'development', }; @@ -21,7 +22,7 @@ const mysqlConfig: DataSourceOptions = { username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'kantancms', - entities: [User, Post, Customer], + entities: [User, Post, Customer, ContactRecord], synchronize: false, // Always false in production logging: process.env.NODE_ENV === 'development', }; @@ -34,7 +35,7 @@ const postgresConfig: DataSourceOptions = { username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'kantancms', - entities: [User, Post, Customer], + entities: [User, Post, Customer, ContactRecord], synchronize: false, // Always false in production logging: process.env.NODE_ENV === 'development', }; diff --git a/src/lib/database/entities/ContactRecord.ts b/src/lib/database/entities/ContactRecord.ts new file mode 100644 index 0000000..6c9223c --- /dev/null +++ b/src/lib/database/entities/ContactRecord.ts @@ -0,0 +1,27 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import type { Customer } from './Customer'; + +@Entity('contact_records') +export class ContactRecord { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + customerId: string; + + @Column() + contactType: string; + + @Column('text') + notes: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + modifiedAt: Date; + + @ManyToOne('Customer', 'contactRecords') + @JoinColumn({ name: 'customerId' }) + customer: Customer; +} diff --git a/src/lib/database/entities/Customer.ts b/src/lib/database/entities/Customer.ts index 466877a..3ef2154 100644 --- a/src/lib/database/entities/Customer.ts +++ b/src/lib/database/entities/Customer.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import type { ContactRecord } from './ContactRecord'; @Entity('customers') export class Customer { @@ -19,4 +20,7 @@ export class Customer { @UpdateDateColumn() modifiedAt: Date; + + @OneToMany('ContactRecord', 'customer') + contactRecords: ContactRecord[]; } diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index 6f7f8bb..6430f2f 100644 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -35,3 +35,4 @@ export const getDataSource = async () => { export * from './entities/User'; export * from './entities/Post'; export * from './entities/Customer'; +export * from './entities/ContactRecord';