| Duration | ~45 min in the lesson + ~25 min homework |
| Prerequisites | Checkpoint lesson-5.6; the browser connection from 1.6. |
| Checkpoint | lesson-5.7 |
What you will have
The student can add a lead by hand, export the filtered lead list to a CSV file, import leads from a CSV file without creating duplicates, and delete one person's data completely, including notes, tasks and status history.
Video
The video for this lesson is not recorded yet.
Prompts used in this lesson
Part 6 — Building it
Add four data features to the CRM: manual entry, CSV export, CSV import with duplicate detection, and complete deletion of a lead. Purpose: leads also arrive by phone and from old spreadsheets; the owner must be able to take data out and to remove a person's data fully. Context: docs/crm-spec.md, the lead list with its filters, the single status-change function. Add these rules to the spec as "Data in and out". 1. Manual entry: an "Add lead" button on /admin/leads opening a form: name (required), email, phone (at least one of the two required), message, status (default new). Same validation rules as the public form. Saves with created_via = manual. Sends no notifications. A status other than new is recorded through the status function. If the duplicate rule below matches an existing lead, warn with a link; allow "Add anyway". 2. Export: an "Export CSV" button on /admin/leads that downloads every lead matching the current search and filters (all pages), with a header row and these columns: name, email, phone, message, status, created_at, created_via, and the stored source columns. UTF-8. Neutralise cells that a spreadsheet could run as a formula, following OWASP's CSV injection guidance; check that guidance rather than relying on memory. 3. Import: an "Import CSV" button. It accepts a file with the same columns as the export (only name plus email or phone are required; unknown columns are ignored), up to 1,000 rows. Duplicate rule, exactly: tidy emails by trimming and lowercasing; tidy phones by keeping digits only. If a row has an email, it is a duplicate when an existing lead has the same tidy email. If a row has no email, it is a duplicate when an existing lead has the same tidy phone. Rows earlier in the same file count as existing. First show a preview and change nothing: counts of new, duplicate and invalid rows, and a table giving for each duplicate the lead it matched and for each invalid row the reason. Only "Confirm import" writes. Duplicates are skipped, never merged or overwritten. New rows are saved with created_via = import, the status from the file if it is one of the five stages (otherwise new, with any non-new status recorded through the status function), and created_at from the file if valid (otherwise now). No notifications. Undo the export's formula neutralising, so an exported file imports back unchanged. Imported text is shown as plain text. 4. Delete: a "Delete lead" button on the lead card. The dialog states what will be deleted with counts (notes, tasks, history rows) and requires typing the lead's name. Deleting removes the lead and, through the cascade from the first CRM migration, its notes, tasks and history. Verify the cascade exists; if not, stop and tell me. Afterwards list where copies may remain: notification emails, Telegram messages, exported files, provider logs and backups. Constraints: everything on the server for the signed-in owner only. Before you report, verify in a real browser using the browser connection; when the login page appears, wait while I sign in myself. Check and report on each: a manual lead sends no notification; an export with a status filter has as many rows as the list's total; importing that file unchanged previews 0 new; after adding two new people, one row repeating an existing email under a different name and one row with no email and an existing phone spaced differently, the preview says 2 new and 2 more duplicates, and confirming raises the lead total by exactly 2; deleting a lead with notes, tasks and history leaves no rows for its id in any of the four tables. Run the build. Say what you could not verify.
Do along
- Pause at the prompt in Part 6. Run the prompt. Adjust the export columns to the ones you use.
- Add one lead by hand, as if someone had just phoned you. No notification should arrive.
- After Part 7, filter the list, export, and edit the file: rename one existing row; add two new people; add one row with no email and an existing lead's phone number. Save as CSV.
- Import it. Predict the totals before you read the preview.
- Write down the id of a sample lead with a note and a task. Delete it and check the three tables.
- Delete the exported file from your computer.
- Do the "Check your work" steps, then run the commit and tag commands that end the lesson.
Check your work
- Note the total on
/admin/leads. Export with no filters. Expected: the file has that many rows plus the header. - Import the same file unchanged. Expected: preview shows 0 new; every row is a duplicate.
- Import your edited file. Expected: preview shows exactly 2 new; your renamed row and your phone-only row are listed as duplicates with the leads they matched.
- Confirm the import. Expected: the list total is the earlier total plus 2; the renamed lead still has its original name.
- Import the edited file a second time. Expected: 0 new.
- Delete a lead that has a note and a task, having written down its id. Expected: searching the list for its name finds nothing.
- In the Supabase table view, filter
lead_notes,lead_tasksandlead_status_historyby that lead id. Expected: no rows. Or ask Claude: "Count the rows for lead id <id> in all four tables." Expected: four zeros.
Common problems
- Every row is reported invalid. → The spreadsheet saved with a different separator or changed the header names. → "The import rejects this file; here is its header line: <paste the header line only>. Find out why and accept this format."
- Phone numbers changed after editing in a spreadsheet. → Spreadsheet programs may treat phone numbers as numbers and drop a leading plus or zero. → Format the phone column as text before editing, or use a plain text editor.
- After deletion, rows remain in a child table. → The cascade from 5.1 is missing. → Stop using Delete. "Deleting lead <id> left rows in <table>. Add a migration that fixes the foreign key, remove the orphaned rows, and verify with a fresh test lead."
Homework
About 25 minutes, on your own, after the lesson. No later lesson depends on it.
- Your real contacts, prepared but not imported. Put your existing contacts into a CSV file with the export's header row, kept outside the project folder so it is never committed. Run Import as far as the preview, fix the file until nothing is invalid, then close the preview without confirming: real contacts go in when you go live. Deliverable: a clean file. Done when: the preview shows 0 invalid rows and the lead total is unchanged.
- Your deletion routine. List every place a person's data lives in your business beyond the CRM, in the order you would clear them. Not legal advice. Deliverable: a section "Deletion requests" in
docs/crm-habits.md(your own notes; create it if missing). Done when: it covers every place in the CRM's reminder plus your own, such as phone contacts or paper.
If a task changed files in the project, commit and push without a tag.
Save your work
git add -A
git commit -m "Lesson 5.7: manual entry, CSV export and import with duplicate detection, full deletion"
git tag lesson-5.7
git push
git push --tags