Skip to content

Commit 290c3bd

Browse files
JacobCoffeeclaude
andauthored
Add quick fields to lead note modal (#254)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b3e2ad2 commit 290c3bd

3 files changed

Lines changed: 230 additions & 33 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
.scan-header {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
padding: 8px 0 16px;
6+
text-align: center;
7+
}
8+
9+
.scan-header-icon {
10+
font-size: 32px;
11+
margin-bottom: 4px;
12+
}
13+
14+
.scan-header h2 {
15+
margin: 0;
16+
font-size: 1.3rem;
17+
font-weight: 700;
18+
}
19+
20+
.scan-header-sponsor {
21+
font-size: 0.8rem;
22+
color: #5833E9;
23+
margin: 2px 0 0;
24+
}
25+
26+
.field-section {
27+
margin-bottom: 20px;
28+
}
29+
30+
.field-label {
31+
display: block;
32+
font-size: 0.7rem;
33+
font-weight: 600;
34+
text-transform: uppercase;
35+
letter-spacing: 0.06em;
36+
color: var(--ion-color-medium);
37+
margin-bottom: 8px;
38+
}
39+
40+
/* Interest buttons */
41+
.interest-group {
42+
display: flex;
43+
gap: 8px;
44+
}
45+
46+
.interest-btn {
47+
flex: 1;
48+
padding: 12px 8px;
49+
border: 2px solid var(--ion-color-step-200, #e0e0e0);
50+
border-radius: 12px;
51+
background: transparent;
52+
font-size: 0.9rem;
53+
font-weight: 600;
54+
color: var(--ion-text-color);
55+
cursor: pointer;
56+
transition: all 0.15s ease;
57+
58+
&.selected.hot {
59+
border-color: #ef4444;
60+
background: rgba(239, 68, 68, 0.08);
61+
color: #ef4444;
62+
}
63+
64+
&.selected.warm {
65+
border-color: #f59e0b;
66+
background: rgba(245, 158, 11, 0.08);
67+
color: #f59e0b;
68+
}
69+
70+
&.selected.cold {
71+
border-color: #3b82f6;
72+
background: rgba(59, 130, 246, 0.08);
73+
color: #3b82f6;
74+
}
75+
}
76+
77+
/* Follow-up chips */
78+
.chip-group {
79+
display: flex;
80+
flex-wrap: wrap;
81+
gap: 6px;
82+
}
83+
84+
.followup-chip {
85+
padding: 8px 14px;
86+
border: 1.5px solid var(--ion-color-step-200, #e0e0e0);
87+
border-radius: 20px;
88+
background: transparent;
89+
font-size: 0.85rem;
90+
color: var(--ion-text-color);
91+
cursor: pointer;
92+
transition: all 0.15s ease;
93+
94+
&.selected {
95+
border-color: #5833E9;
96+
background: rgba(88, 51, 233, 0.08);
97+
color: #5833E9;
98+
font-weight: 600;
99+
}
100+
}
101+
102+
/* Note textarea */
103+
.note-textarea {
104+
--background: var(--ion-color-step-50, #f5f5f5);
105+
--padding-start: 14px;
106+
--padding-end: 14px;
107+
--padding-top: 12px;
108+
--padding-bottom: 12px;
109+
border-radius: 12px;
110+
font-size: 0.95rem;
111+
border: 1.5px solid var(--ion-color-step-150, #e8e8e8);
112+
}
Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,67 @@
1-
<ion-header>
1+
<ion-header class="ion-no-border">
22
<ion-toolbar>
33
<ion-buttons slot="start">
44
<ion-button color="medium" (click)="cancel()">Cancel</ion-button>
55
</ion-buttons>
6-
<ion-title>Make a note</ion-title>
6+
<ion-title>Lead Notes</ion-title>
77
<ion-buttons slot="end">
8-
<ion-button (click)="confirm()">Save</ion-button>
8+
<ion-button (click)="confirm()" color="primary"><strong>Save</strong></ion-button>
99
</ion-buttons>
1010
</ion-toolbar>
1111
</ion-header>
12+
1213
<ion-content class="ion-padding">
13-
<ion-item>
14-
<ion-label>
15-
<ion-grid>
16-
<ion-row>
17-
<ion-col>
18-
<h1>
19-
<ion-icon
20-
[color]="(scan.status === 'captured')? 'success' : 'default'"
21-
[name]="(scan.status === 'captured')? 'id-card-outline' : 'qr-code'">
22-
</ion-icon>
23-
{{(scan.status === 'captured')? scan.first_name : scan.access_code}}
24-
</h1>
25-
</ion-col>
26-
</ion-row>
27-
</ion-grid>
28-
</ion-label>
29-
</ion-item>
30-
<ion-item>
31-
<ion-label position="stacked">Note</ion-label>
14+
<!-- Scan info -->
15+
<div class="scan-header">
16+
<ion-icon
17+
[color]="(scan.status === 'captured') ? 'success' : 'medium'"
18+
[name]="(scan.status === 'captured') ? 'checkmark-circle' : 'ellipsis-horizontal-circle'"
19+
class="scan-header-icon">
20+
</ion-icon>
21+
<h2>{{ (scan.status === 'captured') ? scan.first_name : scan.access_code }}</h2>
22+
<p *ngIf="scan.sponsor_name" class="scan-header-sponsor">{{ scan.sponsor_name }}</p>
23+
</div>
24+
25+
<!-- Interest level -->
26+
<div class="field-section">
27+
<ion-label class="field-label">Interest Level</ion-label>
28+
<div class="interest-group">
29+
<button class="interest-btn" [class.selected]="interestLevel === 'hot'" [class.hot]="interestLevel === 'hot'" (click)="interestLevel = interestLevel === 'hot' ? '' : 'hot'">
30+
🔥 Hot
31+
</button>
32+
<button class="interest-btn" [class.selected]="interestLevel === 'warm'" [class.warm]="interestLevel === 'warm'" (click)="interestLevel = interestLevel === 'warm' ? '' : 'warm'">
33+
☀️ Warm
34+
</button>
35+
<button class="interest-btn" [class.selected]="interestLevel === 'cold'" [class.cold]="interestLevel === 'cold'" (click)="interestLevel = interestLevel === 'cold' ? '' : 'cold'">
36+
❄️ Cold
37+
</button>
38+
</div>
39+
</div>
40+
41+
<!-- Follow-up actions -->
42+
<div class="field-section">
43+
<ion-label class="field-label">Follow-up</ion-label>
44+
<div class="chip-group">
45+
<button *ngFor="let action of followUpActions"
46+
class="followup-chip"
47+
[class.selected]="action.checked"
48+
(click)="action.checked = !action.checked">
49+
{{ action.emoji }} {{ action.label }}
50+
</button>
51+
</div>
52+
</div>
53+
54+
<!-- Free-form notes -->
55+
<div class="field-section">
56+
<ion-label class="field-label">Notes</ion-label>
3257
<ion-textarea
33-
[(ngModel)]="note"
34-
placeholder="Write your notes here..."
35-
maxlength=3000
36-
autoGrow=true
37-
inputmode="text"></ion-textarea>
38-
</ion-item>
58+
[(ngModel)]="freeformNote"
59+
placeholder="Additional notes..."
60+
maxlength="2000"
61+
[autoGrow]="true"
62+
rows="3"
63+
inputmode="text"
64+
class="note-textarea">
65+
</ion-textarea>
66+
</div>
3967
</ion-content>

src/app/lead-note-modal/lead-note-modal.component.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,75 @@ export class LeadNoteModalComponent implements OnInit {
1010
@Input() scan: any;
1111
@Input() note: string;
1212

13-
constructor(private modalCtrl: ModalController) { }
13+
interestLevel: string = '';
14+
freeformNote: string = '';
15+
followUpActions = [
16+
{ label: 'Send info', emoji: '📧', checked: false },
17+
{ label: 'Demo', emoji: '🎯', checked: false },
18+
{ label: 'Newsletter', emoji: '📰', checked: false },
19+
{ label: 'Hiring', emoji: '💼', checked: false },
20+
{ label: 'Partner', emoji: '🤝', checked: false },
21+
];
22+
23+
constructor(private modalCtrl: ModalController) {}
24+
25+
ngOnInit(): void {
26+
this.parseNote(this.note || '');
27+
}
1428

1529
cancel() {
16-
return this.modalCtrl.dismiss({note: null}, 'cancel');
30+
return this.modalCtrl.dismiss({ note: null }, 'cancel');
1731
}
1832

1933
confirm() {
20-
this.modalCtrl.dismiss({note: this.note}, 'save');
34+
const composed = this.composeNote();
35+
this.modalCtrl.dismiss({ note: composed }, 'save');
2136
}
2237

23-
ngOnInit(): void {
24-
console.log(this.scan, this.note);
38+
/** Parse an existing note back into structured fields */
39+
private parseNote(raw: string) {
40+
if (!raw) return;
41+
42+
// Try to parse structured format
43+
const interestMatch = raw.match(/Interest: (hot|warm|cold)/i);
44+
if (interestMatch) {
45+
this.interestLevel = interestMatch[1].toLowerCase();
46+
}
47+
48+
const followUpMatch = raw.match(/Follow-up: (.+)/i);
49+
if (followUpMatch) {
50+
const actions = followUpMatch[1].split(', ').map(a => a.trim().toLowerCase());
51+
this.followUpActions.forEach(a => {
52+
a.checked = actions.includes(a.label.toLowerCase());
53+
});
54+
}
55+
56+
const notesMatch = raw.match(/Notes: ([\s\S]*?)$/m);
57+
if (notesMatch) {
58+
this.freeformNote = notesMatch[1].trim();
59+
} else if (!interestMatch && !followUpMatch) {
60+
// Legacy plain text note — put it all in freeform
61+
this.freeformNote = raw;
62+
}
2563
}
2664

65+
/** Compose structured fields into a single note string */
66+
private composeNote(): string {
67+
const parts: string[] = [];
68+
69+
if (this.interestLevel) {
70+
parts.push(`Interest: ${this.interestLevel}`);
71+
}
72+
73+
const selectedActions = this.followUpActions.filter(a => a.checked).map(a => a.label);
74+
if (selectedActions.length > 0) {
75+
parts.push(`Follow-up: ${selectedActions.join(', ')}`);
76+
}
77+
78+
if (this.freeformNote?.trim()) {
79+
parts.push(`Notes: ${this.freeformNote.trim()}`);
80+
}
81+
82+
return parts.join('\n');
83+
}
2784
}

0 commit comments

Comments
 (0)