feat(opportunities): Link zur Ausschreibung als eigenes Feld
Die Uebernahme aus der Akquise schrieb den Link bisher nur in die Notizen. Jetzt hat die Opportunity eine Spalte `link`, gefuellt bei Uebernahme und Kopie; im Dialog steht sie rechts neben der Phase mit Knopf "Oeffnen". schema.sql traegt den Link fuer aeltere Uebernahmen nach. Die Pruefung liegt in server/src/link.js und gilt fuer Akquise und Opportunities gleich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3e76a73f2e
commit
8cb5cbc7cc
@@ -39,6 +39,7 @@ interface FormState {
|
||||
title: string;
|
||||
company: string;
|
||||
consultant: string;
|
||||
link: string;
|
||||
description: string;
|
||||
stage: OpportunityStage;
|
||||
probability: string;
|
||||
@@ -65,6 +66,7 @@ function toFormState(opportunity: Opportunity | null, fallbackOwnerId: number |
|
||||
title: opportunity?.title ?? '',
|
||||
company: opportunity?.company ?? '',
|
||||
consultant: opportunity?.consultant ?? '',
|
||||
link: opportunity?.link ?? '',
|
||||
description: opportunity?.description ?? '',
|
||||
stage: opportunity?.stage ?? 'Neu',
|
||||
probability: String(opportunity?.probability ?? 10),
|
||||
@@ -137,6 +139,19 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
|
||||
const set = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// Nur http(s) wird anklickbar - wie die Pruefung im Server.
|
||||
const linkHref = useMemo(() => {
|
||||
const text = form.link.trim();
|
||||
if (!text) return null;
|
||||
const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(text) ? text : `https://${text}`;
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [form.link]);
|
||||
|
||||
const leadById = useMemo(() => new Map(leads.map((lead) => [lead.id, lead])), [leads]);
|
||||
|
||||
const addableLeads = useMemo(() => {
|
||||
@@ -201,6 +216,7 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
|
||||
title: form.title.trim(),
|
||||
company: form.company.trim() || null,
|
||||
consultant: form.consultant.trim() || null,
|
||||
link: form.link.trim() || null,
|
||||
description: form.description.trim() || null,
|
||||
stage: form.stage,
|
||||
probability: Number(form.probability) || 0,
|
||||
@@ -408,6 +424,41 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/*
|
||||
Link zur Ausschreibung. Kommt bei der Uebernahme aus der Akquise mit;
|
||||
der Knopf oeffnet ihn in einem neuen Tab. Ohne Schema ergaenzt der
|
||||
Server "https://" - der Knopf tut dasselbe, damit er schon vor dem
|
||||
Speichern funktioniert.
|
||||
*/}
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="opp-link">
|
||||
Link
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id="opp-link"
|
||||
inputMode="url"
|
||||
value={form.link}
|
||||
onChange={(event) => set('link', event.target.value)}
|
||||
placeholder="https://..."
|
||||
className={inputClass}
|
||||
/>
|
||||
{linkHref && (
|
||||
<a
|
||||
href={linkHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Link in neuem Tab öffnen"
|
||||
className="flex shrink-0 items-center justify-center gap-2 rounded-xl border border-ink-200 px-4 text-sm font-bold text-ink-600 transition hover:border-brand-300 hover:text-brand-700"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Öffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DictateTextarea
|
||||
|
||||
@@ -322,6 +322,8 @@ export interface Opportunity {
|
||||
acquisition_id: number | null;
|
||||
/** Der Eintrag existiert noch und liegt nicht im Papierkorb: der Rueckweg ist offen. */
|
||||
acquisition_available?: boolean;
|
||||
/** Link zur Ausschreibung, bei der Uebernahme aus der Akquise mitgenommen. */
|
||||
link: string | null;
|
||||
description: string | null;
|
||||
stage: OpportunityStage;
|
||||
/** 0-100 */
|
||||
|
||||
Reference in New Issue
Block a user