save work

This commit is contained in:
Ken Yasue
2025-04-01 17:23:15 +02:00
parent 2ee0063eb5
commit 6eea8a9d21
7 changed files with 127 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,9 @@ export class Customer {
@Column() @Column()
email: string; email: string;
@Column({ nullable: true })
city: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;

View File

@ -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++; // Check if customer exists by email or name
continue; 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
customer = new Customer();
console.log(`Creating new customer: ${name}`);
} }
// Skip if name already exists in database // Update customer fields
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);
// Create new customer
const customer = new Customer();
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);
importedCount++;
console.log(`Imported customer: ${name}`); if (isUpdate) {
updatedCount++;
console.log(`Updated customer: ${name}`);
} else {
importedCount++;
console.log(`Imported customer: ${name}`);
// Add to lookup maps for future reference
if (email) {
customersByEmail.set(email.toLowerCase(), customer);
}
customersByName.set(name.toLowerCase(), customer);
}
} catch (e) { } catch (e) {
console.log(`Skipped: ${name}`); 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);