save work
This commit is contained in:
@ -9,6 +9,7 @@ interface Customer {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
city?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
modifiedAt: string;
|
modifiedAt: string;
|
||||||
}
|
}
|
||||||
@ -23,6 +24,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
city: '',
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(!!id); // Only loading if editing
|
const [isLoading, setIsLoading] = useState(!!id); // Only loading if editing
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -49,6 +51,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
name: customer.name,
|
name: customer.name,
|
||||||
url: customer.url || '',
|
url: customer.url || '',
|
||||||
email: customer.email,
|
email: customer.email,
|
||||||
|
city: customer.city || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -169,7 +172,7 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-4">
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
@ -185,6 +188,21 @@ export default function EditCustomer({ id }: EditCustomerProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="city">
|
||||||
|
City
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
|
id="city"
|
||||||
|
type="text"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="City (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<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"
|
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"
|
||||||
|
|||||||
@ -91,6 +91,12 @@ export default async function CustomerDetail({ params }: { params: { id: string
|
|||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</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">City</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{customer.city || <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">
|
<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>
|
<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">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ interface Customer {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
city?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
modifiedAt: string;
|
modifiedAt: string;
|
||||||
}
|
}
|
||||||
@ -44,6 +45,7 @@ export default function AdminCustomers() {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
city: '',
|
||||||
hasEmail: false
|
hasEmail: false
|
||||||
});
|
});
|
||||||
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
||||||
@ -74,6 +76,7 @@ export default function AdminCustomers() {
|
|||||||
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
||||||
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
||||||
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
||||||
|
if (debouncedFilters.city) params.append('city', debouncedFilters.city);
|
||||||
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
||||||
|
|
||||||
const response = await fetch(`/api/customers?${params.toString()}`);
|
const response = await fetch(`/api/customers?${params.toString()}`);
|
||||||
@ -116,6 +119,7 @@ export default function AdminCustomers() {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
city: '',
|
||||||
hasEmail: false
|
hasEmail: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -238,6 +242,31 @@ export default function AdminCustomers() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="city-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search by City
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="city-filter"
|
||||||
|
value={filters.city}
|
||||||
|
onChange={(e) => handleFilterChange('city', e.target.value)}
|
||||||
|
className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||||
|
placeholder="Enter city..."
|
||||||
|
/>
|
||||||
|
{filters.city && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('city', '')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
Email Filter
|
Email Filter
|
||||||
@ -256,12 +285,13 @@ export default function AdminCustomers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(filters.name || filters.email || filters.url || filters.hasEmail) && (
|
{(filters.name || filters.email || filters.url || filters.city || filters.hasEmail) && (
|
||||||
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Active filters: {[
|
Active filters: {[
|
||||||
filters.name && 'Name',
|
filters.name && 'Name',
|
||||||
filters.email && 'Email',
|
filters.email && 'Email',
|
||||||
filters.url && 'URL',
|
filters.url && 'URL',
|
||||||
|
filters.city && 'City',
|
||||||
filters.hasEmail && 'Has Email'
|
filters.hasEmail && 'Has Email'
|
||||||
].filter(Boolean).join(', ')}
|
].filter(Boolean).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +351,12 @@ export default function AdminCustomers() {
|
|||||||
>
|
>
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
City
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
||||||
@ -380,6 +416,9 @@ export default function AdminCustomers() {
|
|||||||
{customer.email}
|
{customer.email}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{customer.city || <span className="text-gray-400 dark:text-gray-500">-</span>}
|
||||||
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{new Date(customer.createdAt).toLocaleDateString()}
|
{new Date(customer.createdAt).toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
@ -399,7 +438,7 @@ export default function AdminCustomers() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="py-4 pl-4 pr-3 text-sm text-gray-500 dark:text-gray-400 text-center">
|
<td colSpan={8} className="py-4 pl-4 pr-3 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||||
No customers found. Create your first customer!
|
No customers found. Create your first customer!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const { name, url, email } = data;
|
const { name, url, email, city } = data;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name || !email) {
|
if (!name || !email) {
|
||||||
@ -69,6 +69,7 @@ export async function PUT(
|
|||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = url || '';
|
customer.url = url || '';
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = city || null;
|
||||||
|
|
||||||
// Save the updated customer
|
// Save the updated customer
|
||||||
const updatedCustomer = await customerRepository.save(customer);
|
const updatedCustomer = await customerRepository.save(customer);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const nameFilter = url.searchParams.get('name');
|
const nameFilter = url.searchParams.get('name');
|
||||||
const emailFilter = url.searchParams.get('email');
|
const emailFilter = url.searchParams.get('email');
|
||||||
const urlFilter = url.searchParams.get('url');
|
const urlFilter = url.searchParams.get('url');
|
||||||
|
const cityFilter = url.searchParams.get('city');
|
||||||
const hasEmailFilter = url.searchParams.get('hasEmail');
|
const hasEmailFilter = url.searchParams.get('hasEmail');
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
@ -34,6 +35,9 @@ export async function GET(request: NextRequest) {
|
|||||||
if (urlFilter) {
|
if (urlFilter) {
|
||||||
queryBuilder = queryBuilder.andWhere('LOWER(customer.url) LIKE LOWER(:url)', { url: `%${urlFilter}%` });
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.url) LIKE LOWER(:url)', { url: `%${urlFilter}%` });
|
||||||
}
|
}
|
||||||
|
if (cityFilter) {
|
||||||
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.city) LIKE LOWER(:city)', { city: `%${cityFilter}%` });
|
||||||
|
}
|
||||||
if (hasEmailFilter === 'true') {
|
if (hasEmailFilter === 'true') {
|
||||||
queryBuilder = queryBuilder.andWhere('customer.email IS NOT NULL AND customer.email != :emptyString', { emptyString: '' });
|
queryBuilder = queryBuilder.andWhere('customer.email IS NOT NULL AND customer.email != :emptyString', { emptyString: '' });
|
||||||
}
|
}
|
||||||
@ -80,7 +84,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const customerRepository = dataSource.getRepository(Customer);
|
const customerRepository = dataSource.getRepository(Customer);
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const { name, url, email } = data;
|
const { name, url, email, city } = data;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name || !email) {
|
if (!name || !email) {
|
||||||
@ -95,6 +99,7 @@ export async function POST(request: NextRequest) {
|
|||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = url || '';
|
customer.url = url || '';
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = city || null;
|
||||||
|
|
||||||
const savedCustomer = await customerRepository.save(customer);
|
const savedCustomer = await customerRepository.save(customer);
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export class Customer {
|
|||||||
@Column()
|
@Column()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
city: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@ -28,65 +28,78 @@ async function importCustomers(csvFilePath: string): Promise<void> {
|
|||||||
|
|
||||||
console.log(`Found ${records.length} records in CSV file`);
|
console.log(`Found ${records.length} records in CSV file`);
|
||||||
|
|
||||||
// Track processed emails to skip duplicates
|
// Get existing customers for update
|
||||||
const processedEmails = new Set<string>();
|
const existingCustomers = await customerRepository.find();
|
||||||
// Track existing names to ensure uniqueness
|
const customersByEmail = new Map<string, Customer>();
|
||||||
const existingNames = new Set<string>(
|
const customersByName = new Map<string, Customer>();
|
||||||
(await customerRepository.find()).map(customer => customer.name)
|
|
||||||
);
|
// Create lookup maps for faster access
|
||||||
|
existingCustomers.forEach(customer => {
|
||||||
|
if (customer.email) {
|
||||||
|
customersByEmail.set(customer.email.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
customersByName.set(customer.name.toLowerCase(), customer);
|
||||||
|
});
|
||||||
|
|
||||||
let importedCount = 0;
|
let importedCount = 0;
|
||||||
let skippedDuplicateEmail = 0;
|
let updatedCount = 0;
|
||||||
let skippedDuplicateName = 0;
|
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const email = record.Email === 'null' ? '' : record.Email;
|
const email = record.Email === 'null' ? '' : record.Email;
|
||||||
const name = record.Name;
|
const name = record.Name;
|
||||||
|
|
||||||
// Skip if email is already processed (not empty and already seen)
|
let customer: Customer;
|
||||||
if (email && processedEmails.has(email)) {
|
let isUpdate = false;
|
||||||
console.log(`Skipping record with duplicate email: ${email}`);
|
|
||||||
skippedDuplicateEmail++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if name already exists in database
|
|
||||||
if (existingNames.has(name)) {
|
|
||||||
console.log(`Skipping record with duplicate name: ${name}`);
|
|
||||||
skippedDuplicateName++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to processed sets
|
|
||||||
if (email) {
|
|
||||||
processedEmails.add(email);
|
|
||||||
}
|
|
||||||
existingNames.add(name);
|
|
||||||
|
|
||||||
|
// Check if customer exists by email or name
|
||||||
|
if (email && customersByEmail.has(email.toLowerCase())) {
|
||||||
|
// Update existing customer by email
|
||||||
|
customer = customersByEmail.get(email.toLowerCase())!;
|
||||||
|
isUpdate = true;
|
||||||
|
console.log(`Updating customer with email: ${email}`);
|
||||||
|
} else if (customersByName.has(name.toLowerCase())) {
|
||||||
|
// Update existing customer by name
|
||||||
|
customer = customersByName.get(name.toLowerCase())!;
|
||||||
|
isUpdate = true;
|
||||||
|
console.log(`Updating customer with name: ${name}`);
|
||||||
|
} else {
|
||||||
// Create new customer
|
// Create new customer
|
||||||
const customer = new Customer();
|
customer = new Customer();
|
||||||
|
console.log(`Creating new customer: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update customer fields
|
||||||
customer.name = name;
|
customer.name = name;
|
||||||
customer.url = record['URL'] === 'null' ? '' : record['URL'];
|
customer.url = record.URL === 'null' ? '' : record.URL;
|
||||||
customer.email = email;
|
customer.email = email;
|
||||||
|
customer.city = record.City === 'null' ? '' : record.City;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
await customerRepository.save(customer);
|
await customerRepository.save(customer);
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
updatedCount++;
|
||||||
|
console.log(`Updated customer: ${name}`);
|
||||||
|
} else {
|
||||||
importedCount++;
|
importedCount++;
|
||||||
|
|
||||||
console.log(`Imported customer: ${name}`);
|
console.log(`Imported customer: ${name}`);
|
||||||
} catch (e) {
|
|
||||||
console.log(`Skipped: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Add to lookup maps for future reference
|
||||||
|
if (email) {
|
||||||
|
customersByEmail.set(email.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
customersByName.set(name.toLowerCase(), customer);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error saving customer: ${name}`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Import summary:');
|
console.log('Import summary:');
|
||||||
console.log(`- Total records in CSV: ${records.length}`);
|
console.log(`- Total records in CSV: ${records.length}`);
|
||||||
console.log(`- Successfully imported: ${importedCount}`);
|
console.log(`- Successfully imported (new): ${importedCount}`);
|
||||||
console.log(`- Skipped (duplicate email): ${skippedDuplicateEmail}`);
|
console.log(`- Successfully updated: ${updatedCount}`);
|
||||||
console.log(`- Skipped (duplicate name): ${skippedDuplicateName}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing customers:', error);
|
console.error('Error importing customers:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user