5 Commits

Author SHA1 Message Date
ba479a671c save work 2025-03-25 11:29:54 +01:00
42f2a30610 contact recoreds done 2025-03-25 08:12:33 +01:00
c9751b058f contact recoed list 2025-03-25 07:08:43 +01:00
d713939730 detail fixes 2025-03-25 07:02:21 +01:00
1866d84a86 Add customer and contact record management features 2025-03-25 06:49:21 +01:00
21 changed files with 1565 additions and 6 deletions

View File

@ -0,0 +1,5 @@
I want add list page for Contact Recored.
- Add link to contact recored list page to admin console headder
- Users can see customer name, contact type, and notes
- Users can click on customer name then opens the customer detail
- Users can filter by customer, date ( from - to )

View File

@ -0,0 +1,7 @@
I want add EmailTemplate Model
Tablename: EmailTemplate
id, content, modifiedAt, createdAt
Create branch features/emailtemplate
Make CRUD operation UI in Admin console
Then commit changes

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,219 @@
'use client';
import { useState, useEffect, FormEvent, ChangeEvent } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { CONTACT_TYPES } from '@/lib/constants';
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 dark:text-gray-100">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 dark:bg-red-900/20 dark:border-red-500">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400 dark:text-red-300" 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 dark:text-red-300">{error}</p>
</div>
</div>
</div>
)}
<div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 dark:bg-gray-800">
<div className="mb-6">
<label className="block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2">
Customer
</label>
<div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-700 dark:border-gray-600 leading-tight">
{customerName}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="contactType" className="block text-gray-700 dark:text-gray-300 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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"
required
>
<option value="">Select a contact type</option>
{CONTACT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
<div>
<label htmlFor="notes" className="block text-gray-700 dark:text-gray-300 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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"
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,317 @@
'use client';
import { useState, useEffect, FormEvent } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { CONTACT_TYPES } from '@/lib/constants';
interface Customer {
id: string;
name: string;
}
interface ContactRecord {
id: string;
customerId: string;
contactType: string;
notes: string;
createdAt: string;
customer: Customer;
}
export default function ContactRecordsList() {
const router = useRouter();
const searchParams = useSearchParams();
// Get filter values from URL params
const initialCustomerId = searchParams.get('customerId') || '';
const initialContactType = searchParams.get('contactType') || '';
const initialDateFrom = searchParams.get('dateFrom') || '';
const initialDateTo = searchParams.get('dateTo') || '';
// State for filters
const [customerId, setCustomerId] = useState(initialCustomerId);
const [contactType, setContactType] = useState(initialContactType);
const [dateFrom, setDateFrom] = useState(initialDateFrom);
const [dateTo, setDateTo] = useState(initialDateTo);
// State for data
const [contactRecords, setContactRecords] = useState<ContactRecord[]>([]);
const [customers, setCustomers] = useState<Customer[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch customers for the filter dropdown
useEffect(() => {
const fetchCustomers = async () => {
try {
const response = await fetch('/api/customers');
if (!response.ok) {
throw new Error('Failed to fetch customers');
}
const data = await response.json();
setCustomers(data);
} catch (err) {
console.error('Error fetching customers:', err);
setError(err instanceof Error ? err.message : 'An error occurred');
}
};
fetchCustomers();
}, []);
// Fetch contact records with filters
useEffect(() => {
const fetchContactRecords = async () => {
setIsLoading(true);
setError(null);
try {
// Build query string with filters
const params = new URLSearchParams();
if (customerId) params.append('customerId', customerId);
if (contactType) params.append('contactType', contactType);
if (dateFrom) params.append('dateFrom', dateFrom);
if (dateTo) params.append('dateTo', dateTo);
const response = await fetch(`/api/contact-records?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch contact records');
}
const data = await response.json();
setContactRecords(data);
} catch (err) {
console.error('Error fetching contact records:', err);
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
}
};
// Only fetch if we have URL parameters or if this is the initial load
if (initialCustomerId || initialContactType || initialDateFrom || initialDateTo || isLoading) {
fetchContactRecords();
}
}, [initialCustomerId, initialContactType, initialDateFrom, initialDateTo]);
// Handle filter form submission
const handleFilterSubmit = (e: FormEvent) => {
e.preventDefault();
// Build query string with filters
const params = new URLSearchParams();
if (customerId) params.append('customerId', customerId);
if (contactType) params.append('contactType', contactType);
if (dateFrom) params.append('dateFrom', dateFrom);
if (dateTo) params.append('dateTo', dateTo);
// Update URL with filters
router.push(`/admin/contact-records?${params.toString()}`);
};
// Handle filter reset
const handleReset = () => {
setCustomerId('');
setContactType('');
setDateFrom('');
setDateTo('');
router.push('/admin/contact-records');
};
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">Contact Records</h1>
</div>
{/* Filter Form */}
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg mb-8">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Filter Contact Records</h3>
</div>
<div className="px-4 py-5 sm:p-6">
<form onSubmit={handleFilterSubmit} className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
<div>
<label htmlFor="customerId" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Customer
</label>
<select
id="customerId"
name="customerId"
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
>
<option value="">All Customers</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="contactType" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Contact Type
</label>
<select
id="contactType"
name="contactType"
value={contactType}
onChange={(e) => setContactType(e.target.value)}
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
>
<option value="">All Contact Types</option>
{CONTACT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
<div>
<label htmlFor="dateFrom" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Date From
</label>
<input
type="date"
id="dateFrom"
name="dateFrom"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
/>
</div>
<div>
<label htmlFor="dateTo" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Date To
</label>
<input
type="date"
id="dateTo"
name="dateTo"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600"
>
Reset
</button>
<button
type="submit"
className="inline-flex items-center px-4 py-2 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"
>
Filter
</button>
</div>
</form>
</div>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-4 dark:bg-red-900/20 dark:border-red-500">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400 dark:text-red-300" 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 dark:text-red-300">{error}</p>
</div>
</div>
</div>
)}
{/* Loading State */}
{isLoading ? (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400">Loading contact records...</p>
</div>
) : (
<>
{/* Contact Records Table */}
{contactRecords.length === 0 ? (
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:p-6 text-center">
<p className="text-gray-500 dark:text-gray-400">No contact records found.</p>
</div>
</div>
) : (
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Customer
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Contact Type
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Notes
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Date
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{contactRecords.map((record) => (
<tr key={record.id}>
<td className="px-6 py-4 whitespace-nowrap">
<Link
href={`/admin/customers/detail/${record.customerId}`}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{record.customer?.name || 'Unknown Customer'}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
{record.contactType}
</span>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900 dark:text-gray-200 max-w-xs truncate">
{record.notes || <span className="text-gray-400 dark:text-gray-500 italic">No notes</span>}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(record.createdAt).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link
href={`/admin/contact-records/edit/${record.id}`}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-4"
>
Edit
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</>
)}
</div>
);
}

View File

@ -0,0 +1,165 @@
'use client';
import { useState, useEffect } 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 {
customerId: string;
}
export default function ContactRecordList({ customerId }: ContactRecordListProps) {
const router = useRouter();
const [contactRecords, setContactRecords] = useState<ContactRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isDeleting, setIsDeleting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Function to fetch contact records
const fetchContactRecords = async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/contact-records?customerId=${customerId}`);
if (!response.ok) {
throw new Error('Failed to fetch contact records');
}
const data = await response.json();
setContactRecords(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred while fetching contact records');
} finally {
setIsLoading(false);
}
};
// Fetch records on initial load and when customerId changes
useEffect(() => {
fetchContactRecords();
}, [customerId]);
// Refresh data when router.refresh() is called
useEffect(() => {
// Create a callback function that will be called when the component is re-rendered
const refreshData = () => {
fetchContactRecords();
};
// Add an event listener for a custom event that will be dispatched when router.refresh() is called
window.addEventListener('contact-records-refresh', refreshData);
return () => {
window.removeEventListener('contact-records-refresh', refreshData);
};
}, []);
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 contact records list
fetchContactRecords();
// Refresh the page
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsDeleting(null);
}
};
if (isLoading) {
return (
<div className="text-center py-8 text-gray-500">
Loading contact records...
</div>
);
}
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,158 @@
'use client';
import { useState, FormEvent, ChangeEvent } from 'react';
import { useRouter } from 'next/navigation';
import { CONTACT_TYPES } from '@/lib/constants';
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);
// Dispatch a custom event to notify the ContactRecordList component to refresh
window.dispatchEvent(new Event('contact-records-refresh'));
// 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 dark:bg-red-900/20 dark:border-red-500">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400 dark:text-red-300" 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 dark:text-red-300">{error}</p>
</div>
</div>
</div>
)}
{success && (
<div className="bg-green-50 border-l-4 border-green-400 p-4 mb-4 dark:bg-green-900/20 dark:border-green-500">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400 dark:text-green-300" 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 dark:text-green-300">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 dark:text-gray-300">
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
required
>
<option value="">Select a contact type</option>
{CONTACT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
<div>
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
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 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
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,132 @@
import Link from 'next/link';
import { getDataSource, Customer } 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 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>
);
}
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 customerId={id} />
</div>
</div>
</div>
);
}

View File

@ -33,6 +33,12 @@ export default async function AdminCustomers() {
<th
scope="col"
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
</th>
@ -70,7 +76,20 @@ export default async function AdminCustomers() {
customers.map((customer) => (
<tr key={customer.id}>
<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 className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{customer.url ? (
@ -113,7 +132,7 @@ export default async function AdminCustomers() {
))
) : (
<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!
</td>
</tr>

View File

@ -106,6 +106,18 @@ export default function RootLayout({
>
Customers
</Link>
<Link
href="/admin/contact-records"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Contact Records
</Link>
<Link
href="/admin/email-templates"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Email Templates
</Link>
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">

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,115 @@
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');
const contactType = url.searchParams.get('contactType');
const dateFrom = url.searchParams.get('dateFrom');
const dateTo = url.searchParams.get('dateTo');
// Build query
const queryOptions: any = {
order: { createdAt: 'DESC' },
relations: ['customer']
};
// Build where clause
let whereClause: any = {};
// Filter by customer if customerId is provided
if (customerId) {
whereClause.customerId = customerId;
}
// Filter by contact type if provided
if (contactType) {
whereClause.contactType = contactType;
}
// Filter by date range if provided
if (dateFrom || dateTo) {
whereClause.createdAt = {};
if (dateFrom) {
whereClause.createdAt.gte = new Date(dateFrom);
}
if (dateTo) {
// Set the date to the end of the day for inclusive filtering
const endDate = new Date(dateTo);
endDate.setHours(23, 59, 59, 999);
whereClause.createdAt.lte = endDate;
}
}
// Add where clause to query options if not empty
if (Object.keys(whereClause).length > 0) {
queryOptions.where = whereClause;
}
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

@ -0,0 +1,122 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDataSource, EmailTemplate } from '@/lib/database';
// GET /api/email-templates/[id] - Get a single email template by ID
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const dataSource = await getDataSource();
const emailTemplateRepository = dataSource.getRepository(EmailTemplate);
const emailTemplate = await emailTemplateRepository.findOne({
where: { id }
});
if (!emailTemplate) {
return NextResponse.json(
{ error: 'Email template not found' },
{ status: 404 }
);
}
return NextResponse.json(emailTemplate);
} catch (error) {
console.error('Error fetching email template:', error);
return NextResponse.json(
{ error: 'Failed to fetch email template' },
{ status: 500 }
);
}
}
// PUT /api/email-templates/[id] - Update an email template
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const dataSource = await getDataSource();
const emailTemplateRepository = dataSource.getRepository(EmailTemplate);
// Check if email template exists
const emailTemplate = await emailTemplateRepository.findOne({
where: { id }
});
if (!emailTemplate) {
return NextResponse.json(
{ error: 'Email template not found' },
{ status: 404 }
);
}
// Get update data
const data = await request.json();
const { name, content } = data;
// Validate required fields
if (!name && !content) {
return NextResponse.json(
{ error: 'At least one field (name or content) must be provided' },
{ status: 400 }
);
}
// Update fields
if (name) emailTemplate.name = name;
if (content) emailTemplate.content = content;
// Save updated email template
const updatedEmailTemplate = await emailTemplateRepository.save(emailTemplate);
return NextResponse.json(updatedEmailTemplate);
} catch (error) {
console.error('Error updating email template:', error);
return NextResponse.json(
{ error: 'Failed to update email template' },
{ status: 500 }
);
}
}
// DELETE /api/email-templates/[id] - Delete an email template
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const dataSource = await getDataSource();
const emailTemplateRepository = dataSource.getRepository(EmailTemplate);
// Check if email template exists
const emailTemplate = await emailTemplateRepository.findOne({
where: { id }
});
if (!emailTemplate) {
return NextResponse.json(
{ error: 'Email template not found' },
{ status: 404 }
);
}
// Delete the email template
await emailTemplateRepository.remove(emailTemplate);
return NextResponse.json(
{ message: 'Email template deleted successfully' },
{ status: 200 }
);
} catch (error) {
console.error('Error deleting email template:', error);
return NextResponse.json(
{ error: 'Failed to delete email template' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDataSource, EmailTemplate } from '@/lib/database';
// GET /api/email-templates - Get all email templates
export async function GET(request: NextRequest) {
try {
const dataSource = await getDataSource();
const emailTemplateRepository = dataSource.getRepository(EmailTemplate);
// Get query parameters
const url = new URL(request.url);
const search = url.searchParams.get('search');
// Build query
const queryOptions: any = {
order: { createdAt: 'DESC' }
};
// Add search filter if provided
if (search) {
queryOptions.where = [
{ name: search ? { contains: search } : undefined }
];
}
const emailTemplates = await emailTemplateRepository.find(queryOptions);
return NextResponse.json(emailTemplates);
} catch (error) {
console.error('Error fetching email templates:', error);
return NextResponse.json(
{ error: 'Failed to fetch email templates' },
{ status: 500 }
);
}
}
// POST /api/email-templates - Create a new email template
export async function POST(request: NextRequest) {
try {
const dataSource = await getDataSource();
const emailTemplateRepository = dataSource.getRepository(EmailTemplate);
const data = await request.json();
const { name, content } = data;
// Validate required fields
if (!name || !content) {
return NextResponse.json(
{ error: 'Name and content are required' },
{ status: 400 }
);
}
// Create and save the new email template
const emailTemplate = new EmailTemplate();
emailTemplate.name = name;
emailTemplate.content = content;
const savedEmailTemplate = await emailTemplateRepository.save(emailTemplate);
return NextResponse.json(savedEmailTemplate, { status: 201 });
} catch (error) {
console.error('Error creating email template:', error);
return NextResponse.json(
{ error: 'Failed to create email template' },
{ status: 500 }
);
}
}

9
src/lib/constants.ts Normal file
View File

@ -0,0 +1,9 @@
// Contact record types
export const CONTACT_TYPES = [
{ value: 'Email', label: 'Email' },
{ value: 'EmailSent', label: 'Email sent' },
{ value: 'EmailReceived', label: 'Email received' },
{ value: 'Phone', label: 'Phone' },
{ value: 'Meeting', label: 'Meeting' },
{ value: 'Other', label: 'Other' }
];

View File

@ -2,13 +2,15 @@ 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 { EmailTemplate } from './entities/EmailTemplate';
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, EmailTemplate],
synchronize: true, // Set to false in production
logging: process.env.NODE_ENV === 'development',
};
@ -21,7 +23,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, EmailTemplate],
synchronize: false, // Always false in production
logging: process.env.NODE_ENV === 'development',
};
@ -34,7 +36,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, EmailTemplate],
synchronize: false, // Always false in production
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')
export class Customer {
@ -19,4 +20,7 @@ export class Customer {
@UpdateDateColumn()
modifiedAt: Date;
@OneToMany('ContactRecord', 'customer')
contactRecords: ContactRecord[];
}

View File

@ -0,0 +1,19 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('email_templates')
export class EmailTemplate {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column('text')
content: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
modifiedAt: Date;
}

View File

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