Add customer and contact record management features

This commit is contained in:
Ken Yasue
2025-03-25 06:49:21 +01:00
parent 4e9d81924a
commit 1866d84a86
13 changed files with 917 additions and 6 deletions

View File

@ -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

View File

@ -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<string>('');
const [customerName, setCustomerName] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
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 <div className="text-center py-10">Loading...</div>;
}
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-semibold text-gray-900">Edit Contact Record</h1>
<Link
href={`/admin/customers/detail/${customerId}`}
className="text-indigo-600 hover:text-indigo-900"
>
Back to Customer
</Link>
</div>
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
<div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2">
Customer
</label>
<div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight">
{customerName}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="contactType" className="block text-gray-700 text-sm font-bold mb-2">
Contact Type
</label>
<select
id="contactType"
name="contactType"
value={formData.contactType}
onChange={handleChange}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
required
>
<option value="">Select a contact type</option>
<option value="Email">Email</option>
<option value="Phone">Phone</option>
<option value="Meeting">Meeting</option>
<option value="Other">Other</option>
</select>
</div>
<div>
<label htmlFor="notes" className="block text-gray-700 text-sm font-bold mb-2">
Notes
</label>
<textarea
id="notes"
name="notes"
rows={4}
value={formData.notes}
onChange={handleChange}
className="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="Enter notes about the contact"
></textarea>
</div>
<div className="flex items-center justify-between">
<button
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? 'Updating...' : 'Update Contact Record'}
</button>
<Link
href={`/admin/customers/detail/${customerId}`}
className="inline-block align-baseline font-bold text-sm text-indigo-600 hover:text-indigo-800"
>
Cancel
</Link>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,6 @@
import EditContactRecord from '../../components/EditContactRecord';
export default async function EditContactRecordPage({ params }: { params: { id: string } }) {
const { id } = params;
return <EditContactRecord id={id} />;
}

View File

@ -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<string | null>(null);
const [error, setError] = useState<string | null>(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 (
<div className="text-center py-8 text-gray-500">
No contact records found. Add your first contact record above.
</div>
);
}
return (
<div className="overflow-hidden">
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-4 mx-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
<ul className="divide-y divide-gray-200">
{contactRecords.map((record) => (
<li key={record.id} className="px-4 py-4">
<div className="flex justify-between">
<div>
<div className="flex items-center">
<span className="font-medium text-gray-900">{record.contactType}</span>
<span className="ml-2 text-sm text-gray-500">
{new Date(record.createdAt).toLocaleString()}
</span>
</div>
<div className="mt-2 text-sm text-gray-700 whitespace-pre-wrap">
{record.notes || <span className="text-gray-400 italic">No notes</span>}
</div>
</div>
<div className="flex items-start space-x-4">
<Link
href={`/admin/contact-records/edit/${record.id}`}
className="text-indigo-600 hover:text-indigo-900 text-sm"
>
Edit
</Link>
<button
onClick={() => handleDelete(record.id)}
disabled={isDeleting === record.id}
className="text-red-600 hover:text-red-900 text-sm disabled:opacity-50"
>
{isDeleting === record.id ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</li>
))}
</ul>
</div>
);
}

View File

@ -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<string | null>(null);
const [success, setSuccess] = useState(false);
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
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 (
<div>
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
{success && (
<div className="bg-green-50 border-l-4 border-green-400 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-green-700">Contact record created successfully!</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="contactType" className="block text-sm font-medium text-gray-700">
Contact Type
</label>
<select
id="contactType"
name="contactType"
value={formData.contactType}
onChange={handleChange}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
required
>
<option value="">Select a contact type</option>
<option value="Email">Email</option>
<option value="Phone">Phone</option>
<option value="Meeting">Meeting</option>
<option value="Other">Other</option>
</select>
</div>
<div>
<label htmlFor="notes" className="block text-sm font-medium text-gray-700">
Notes
</label>
<textarea
id="notes"
name="notes"
rows={4}
value={formData.notes}
onChange={handleChange}
className="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="Enter notes about the contact"
></textarea>
</div>
<div>
<button
type="submit"
disabled={isSubmitting}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{isSubmitting ? 'Adding...' : 'Add Contact Record'}
</button>
</div>
</form>
</div>
);
}

View File

@ -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 (
<div className="text-center py-10">
<h1 className="text-2xl font-semibold text-gray-900 mb-4">Customer Not Found</h1>
<p className="text-gray-500 mb-6">The customer you're looking for doesn't exist or has been deleted.</p>
<Link
href="/admin/customers"
className="text-indigo-600 hover:text-indigo-900"
>
Back to Customers
</Link>
</div>
);
}
// Fetch contact records for this customer
const contactRecords = await contactRecordRepository.find({
where: { customerId: id },
order: { createdAt: 'DESC' }
});
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-semibold text-gray-900">Customer Details</h1>
<div className="flex space-x-4">
<Link
href={`/admin/customers/edit/${id}`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Edit Customer
</Link>
<Link
href="/admin/customers"
className="text-indigo-600 hover:text-indigo-900"
>
Back to Customers
</Link>
</div>
</div>
{/* Customer Information Card */}
<div className="bg-white shadow overflow-hidden sm:rounded-lg mb-8">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Customer Information</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">Details about the customer.</p>
</div>
<div className="border-t border-gray-200">
<dl>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">ID</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{customer.id}</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Name</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{customer.name}</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<a href={`mailto:${customer.email}`} className="text-indigo-600 hover:text-indigo-900">
{customer.email}
</a>
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Website</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{customer.url ? (
<a
href={customer.url.startsWith('http') ? customer.url : `https://${customer.url}`}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 hover:text-indigo-900"
>
{customer.url}
</a>
) : (
<span className="text-gray-400">-</span>
)}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Created</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{new Date(customer.createdAt).toLocaleString()}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Last Modified</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{new Date(customer.modifiedAt).toLocaleString()}
</dd>
</div>
</dl>
</div>
</div>
{/* Contact Records Section */}
<div className="bg-white shadow overflow-hidden sm:rounded-lg mb-8">
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Contact Records</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">History of communications with this customer.</p>
</div>
</div>
{/* New Contact Record Form */}
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<h4 className="text-md font-medium text-gray-900 mb-4">Add New Contact Record</h4>
<NewContactRecordForm customerId={id} />
</div>
{/* Contact Records List */}
<div className="border-t border-gray-200">
<ContactRecordList contactRecords={contactRecords} />
</div>
</div>
</div>
);
}

View File

@ -33,6 +33,12 @@ export default async function AdminCustomers() {
<th <th
scope="col" scope="col"
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
>
ID
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
> >
Name Name
</th> </th>
@ -70,7 +76,20 @@ export default async function AdminCustomers() {
customers.map((customer) => ( customers.map((customer) => (
<tr key={customer.id}> <tr key={customer.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6"> <td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
{customer.name} <Link
href={`/admin/customers/detail/${customer.id}`}
className="text-indigo-600 hover:text-indigo-900"
>
{customer.id.substring(0, 8)}...
</Link>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<Link
href={`/admin/customers/detail/${customer.id}`}
className="text-indigo-600 hover:text-indigo-900"
>
{customer.name}
</Link>
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{customer.url ? ( {customer.url ? (
@ -113,7 +132,7 @@ export default async function AdminCustomers() {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={6} className="py-4 pl-4 pr-3 text-sm text-gray-500 text-center"> <td colSpan={7} className="py-4 pl-4 pr-3 text-sm text-gray-500 text-center">
No customers found. Create your first customer! No customers found. Create your first customer!
</td> </td>
</tr> </tr>

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -2,13 +2,14 @@ import { DataSource, DataSourceOptions } from 'typeorm';
import { User } from './entities/User'; import { User } from './entities/User';
import { Post } from './entities/Post'; import { Post } from './entities/Post';
import { Customer } from './entities/Customer'; import { Customer } from './entities/Customer';
import { ContactRecord } from './entities/ContactRecord';
import path from 'path'; import path from 'path';
// Default configuration for SQLite (development/testing) // Default configuration for SQLite (development/testing)
const sqliteConfig: DataSourceOptions = { const sqliteConfig: DataSourceOptions = {
type: 'sqlite', type: 'sqlite',
database: path.join(process.cwd(), 'data', 'database.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 synchronize: true, // Set to false in production
logging: process.env.NODE_ENV === 'development', logging: process.env.NODE_ENV === 'development',
}; };
@ -21,7 +22,7 @@ const mysqlConfig: DataSourceOptions = {
username: process.env.DB_USERNAME || 'root', username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '', password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'kantancms', database: process.env.DB_DATABASE || 'kantancms',
entities: [User, Post, Customer], entities: [User, Post, Customer, ContactRecord],
synchronize: false, // Always false in production synchronize: false, // Always false in production
logging: process.env.NODE_ENV === 'development', logging: process.env.NODE_ENV === 'development',
}; };
@ -34,7 +35,7 @@ const postgresConfig: DataSourceOptions = {
username: process.env.DB_USERNAME || 'postgres', username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || '', password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'kantancms', database: process.env.DB_DATABASE || 'kantancms',
entities: [User, Post, Customer], entities: [User, Post, Customer, ContactRecord],
synchronize: false, // Always false in production synchronize: false, // Always false in production
logging: process.env.NODE_ENV === 'development', logging: process.env.NODE_ENV === 'development',
}; };

View File

@ -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;
}

View File

@ -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') @Entity('customers')
export class Customer { export class Customer {
@ -19,4 +20,7 @@ export class Customer {
@UpdateDateColumn() @UpdateDateColumn()
modifiedAt: Date; modifiedAt: Date;
@OneToMany('ContactRecord', 'customer')
contactRecords: ContactRecord[];
} }

View File

@ -35,3 +35,4 @@ export const getDataSource = async () => {
export * from './entities/User'; export * from './entities/User';
export * from './entities/Post'; export * from './entities/Post';
export * from './entities/Customer'; export * from './entities/Customer';
export * from './entities/ContactRecord';