mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
feat(crm): inline stage editing, DealDetail optimization, rename Deals to Vorgänge
- PipelinesPage: Stages können jetzt per Doppelklick oder Stift-Icon inline bearbeitet werden (Name, Farbe) via PATCH endpoint - DealDetailPage: Nutzt jetzt deal.pipeline.stages direkt statt separatem usePipeline() API-Call (Backend liefert alle Stages mit) - UI-Texte: "Deals" → "Vorgänge", "Deal" → "Vorgang" in allen user-facing Strings (Sidebar, Seiten, Modals, Fehlermeldungen) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56a9ed9647
commit
0b78160f33
10 changed files with 270 additions and 59 deletions
|
|
@ -16,6 +16,7 @@ import type {
|
|||
CreatePipelinePayload,
|
||||
UpdatePipelinePayload,
|
||||
CreateStagePayload,
|
||||
UpdateStagePayload,
|
||||
PipelineStage,
|
||||
Activity,
|
||||
CreateActivityPayload,
|
||||
|
|
@ -121,6 +122,14 @@ export const pipelinesApi = {
|
|||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
updateStage: (pipelineId: string, stageId: string, data: UpdateStagePayload) =>
|
||||
api
|
||||
.patch<SingleResponse<PipelineStage>>(
|
||||
`/crm/pipelines/${pipelineId}/stages/${stageId}`,
|
||||
data,
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
removeStage: (pipelineId: string, stageId: string) =>
|
||||
api
|
||||
.delete<SingleResponse<PipelineStage>>(
|
||||
|
|
|
|||
|
|
@ -309,11 +309,11 @@ export function ContactDetailPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Verknüpfte Deals */}
|
||||
{/* Verknüpfte Vorgänge */}
|
||||
{deals.length > 0 && (
|
||||
<div className={styles.card} style={{ marginTop: '1.5rem' }}>
|
||||
<h2 className={styles.cardTitle}>
|
||||
Verknüpfte Deals ({deals.length})
|
||||
Verknüpfte Vorgänge ({deals.length})
|
||||
</h2>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useDeal, usePipeline, useDeleteDeal } from '../hooks';
|
||||
import { useDeal, useDeleteDeal } from '../hooks';
|
||||
import { DealFormModal } from './DealFormModal';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import type { DealStatus } from '../types';
|
||||
|
|
@ -39,10 +39,9 @@ export function DealDetailPage() {
|
|||
|
||||
const deal = data?.data;
|
||||
|
||||
// Pipeline mit allen Stages laden fuer Fortschrittsbalken
|
||||
const { data: pipelineData } = usePipeline(deal?.pipelineId ?? '');
|
||||
const pipelineStages = pipelineData?.data?.stages
|
||||
? [...pipelineData.data.stages].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
// Pipeline-Stages direkt aus dem Deal-Objekt (Backend liefert alle Stages mit)
|
||||
const pipelineStages = deal?.pipeline?.stages
|
||||
? [...deal.pipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
: [];
|
||||
|
||||
const [isEditOpen, setEditOpen] = useState(false);
|
||||
|
|
@ -52,7 +51,7 @@ export function DealDetailPage() {
|
|||
if (error || !deal)
|
||||
return (
|
||||
<p style={{ color: 'var(--color-error)' }}>
|
||||
Deal konnte nicht geladen werden
|
||||
Vorgang konnte nicht geladen werden
|
||||
</p>
|
||||
);
|
||||
|
||||
|
|
@ -78,7 +77,7 @@ export function DealDetailPage() {
|
|||
>
|
||||
<path d="M9 2L4 7l5 5" />
|
||||
</svg>
|
||||
Zurück zu Deals
|
||||
Zurück zu Vorgänge
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
|
|
@ -155,7 +154,7 @@ export function DealDetailPage() {
|
|||
|
||||
{/* Info Card */}
|
||||
<div className={styles.card}>
|
||||
<h2 className={styles.cardTitle}>Deal-Details</h2>
|
||||
<h2 className={styles.cardTitle}>Vorgangs-Details</h2>
|
||||
<div className={styles.infoGrid}>
|
||||
<span className={styles.infoLabel}>Wert</span>
|
||||
<span className={styles.infoValue} style={{ fontWeight: 600 }}>
|
||||
|
|
@ -245,7 +244,7 @@ export function DealDetailPage() {
|
|||
<Modal
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
title="Deal löschen"
|
||||
title="Vorgang löschen"
|
||||
maxWidth="420px"
|
||||
>
|
||||
<p
|
||||
|
|
@ -255,7 +254,7 @@ export function DealDetailPage() {
|
|||
marginBottom: '1.5rem',
|
||||
}}
|
||||
>
|
||||
Soll der Deal <strong>{deal.title}</strong> wirklich gelöscht werden?
|
||||
Soll der Vorgang <strong>{deal.title}</strong> wirklich gelöscht werden?
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ export function DealFormModal({
|
|||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={isEditMode ? 'Deal bearbeiten' : 'Neuer Deal'}
|
||||
title={isEditMode ? 'Vorgang bearbeiten' : 'Neuer Vorgang'}
|
||||
maxWidth="600px"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
|
@ -260,7 +260,7 @@ export function DealFormModal({
|
|||
style={inputStyle}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Deal-Titel"
|
||||
placeholder="Vorgangs-Titel"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export function DealsPage() {
|
|||
if (error)
|
||||
return (
|
||||
<p style={{ color: 'var(--color-error)' }}>
|
||||
Fehler beim Laden der Deals
|
||||
Fehler beim Laden der Vorgänge
|
||||
</p>
|
||||
);
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ export function DealsPage() {
|
|||
marginBottom: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>Deals</h1>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>Vorgänge</h1>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -116,7 +116,7 @@ export function DealsPage() {
|
|||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{pagination?.total ?? 0} Deals gesamt
|
||||
{pagination?.total ?? 0} Vorgänge gesamt
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCreateOpen(true)}
|
||||
|
|
@ -131,7 +131,7 @@ export function DealsPage() {
|
|||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
+ Neuer Deal
|
||||
+ Neuer Vorgang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -229,7 +229,7 @@ export function DealsPage() {
|
|||
color: 'var(--color-text-muted)',
|
||||
}}
|
||||
>
|
||||
Keine Deals gefunden
|
||||
Keine Vorgänge gefunden
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
@ -389,14 +389,14 @@ export function DealsPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal: Neuen Deal anlegen */}
|
||||
{/* Modal: Neuen Vorgang anlegen */}
|
||||
<DealFormModal
|
||||
isOpen={isCreateOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
onSuccess={() => setCreateOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Modal: Deal bearbeiten */}
|
||||
{/* Modal: Vorgang bearbeiten */}
|
||||
<DealFormModal
|
||||
isOpen={!!editingDeal}
|
||||
onClose={() => setEditingDeal(null)}
|
||||
|
|
@ -404,11 +404,11 @@ export function DealsPage() {
|
|||
onSuccess={() => setEditingDeal(null)}
|
||||
/>
|
||||
|
||||
{/* Modal: Deal löschen */}
|
||||
{/* Modal: Vorgang löschen */}
|
||||
<Modal
|
||||
isOpen={!!deletingDeal}
|
||||
onClose={() => setDeletingDeal(null)}
|
||||
title="Deal löschen"
|
||||
title="Vorgang löschen"
|
||||
maxWidth="420px"
|
||||
>
|
||||
<p
|
||||
|
|
@ -418,7 +418,7 @@ export function DealsPage() {
|
|||
marginBottom: '1.5rem',
|
||||
}}
|
||||
>
|
||||
Soll der Deal <strong>{deletingDeal?.title}</strong> wirklich gelöscht
|
||||
Soll der Vorgang <strong>{deletingDeal?.title}</strong> wirklich gelöscht
|
||||
werden?
|
||||
</p>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
CreatePipelinePayload,
|
||||
UpdatePipelinePayload,
|
||||
CreateStagePayload,
|
||||
UpdateStagePayload,
|
||||
CreateActivityPayload,
|
||||
UpdateActivityPayload,
|
||||
} from './types';
|
||||
|
|
@ -203,6 +204,24 @@ export function useDeletePipeline() {
|
|||
});
|
||||
}
|
||||
|
||||
export function useUpdateStage() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
pipelineId,
|
||||
stageId,
|
||||
data,
|
||||
}: {
|
||||
pipelineId: string;
|
||||
stageId: string;
|
||||
data: UpdateStagePayload;
|
||||
}) => pipelinesApi.updateStage(pipelineId, stageId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: crmKeys.pipelines.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddStage() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
|
|
|
|||
|
|
@ -108,6 +108,53 @@
|
|||
background: none;
|
||||
}
|
||||
|
||||
.stageEditInput {
|
||||
flex: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.stageEditColor {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stageEditActions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stageEditBtn {
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stageEditSave {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stageEditCancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.newPipelineForm {
|
||||
background: var(--color-bg-card);
|
||||
border: 2px dashed var(--color-border);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,163 @@ import {
|
|||
useCreatePipeline,
|
||||
useDeletePipeline,
|
||||
useAddStage,
|
||||
useUpdateStage,
|
||||
useRemoveStage,
|
||||
} from '../hooks';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import type { Pipeline } from '../types';
|
||||
import type { Pipeline, PipelineStage } from '../types';
|
||||
import styles from './PipelinesPage.module.css';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Einzelne Stage-Zeile (View + Edit) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function StageRow({
|
||||
stage,
|
||||
pipelineId,
|
||||
}: {
|
||||
stage: PipelineStage;
|
||||
pipelineId: string;
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editName, setEditName] = useState(stage.name);
|
||||
const [editColor, setEditColor] = useState(stage.color);
|
||||
|
||||
const updateStageMutation = useUpdateStage();
|
||||
const removeStageMutation = useRemoveStage();
|
||||
|
||||
const handleSave = () => {
|
||||
const changes: Record<string, string> = {};
|
||||
if (editName.trim() !== stage.name) changes.name = editName.trim();
|
||||
if (editColor !== stage.color) changes.color = editColor;
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStageMutation.mutate(
|
||||
{ pipelineId, stageId: stage.id, data: changes },
|
||||
{ onSuccess: () => setIsEditing(false) },
|
||||
);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditName(stage.name);
|
||||
setEditColor(stage.color);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className={styles.stageItem}>
|
||||
<span className={styles.stageOrder}>{stage.sortOrder + 1}</span>
|
||||
<input
|
||||
type="color"
|
||||
className={styles.stageEditColor}
|
||||
value={editColor}
|
||||
onChange={(e) => setEditColor(e.target.value)}
|
||||
title="Farbe"
|
||||
/>
|
||||
<input
|
||||
className={styles.stageEditInput}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
if (e.key === 'Escape') handleCancel();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div className={styles.stageEditActions}>
|
||||
<button
|
||||
className={`${styles.stageEditBtn} ${styles.stageEditSave}`}
|
||||
onClick={handleSave}
|
||||
disabled={!editName.trim() || updateStageMutation.isPending}
|
||||
>
|
||||
{updateStageMutation.isPending ? '...' : '✓'}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.stageEditBtn} ${styles.stageEditCancel}`}
|
||||
onClick={handleCancel}
|
||||
disabled={updateStageMutation.isPending}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.stageItem}>
|
||||
<span className={styles.stageOrder}>{stage.sortOrder + 1}</span>
|
||||
<span
|
||||
className={styles.stageColor}
|
||||
style={{ background: stage.color }}
|
||||
/>
|
||||
<span
|
||||
className={styles.stageName}
|
||||
onDoubleClick={() => setIsEditing(true)}
|
||||
title="Doppelklick zum Bearbeiten"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{stage.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--color-text-muted)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8125rem',
|
||||
padding: '0 0.25rem',
|
||||
}}
|
||||
title="Stufe bearbeiten"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.3"
|
||||
>
|
||||
<path d="M8.5 2.5l3 3M1.5 9.5l6-6 3 3-6 6H1.5v-3z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
removeStageMutation.mutate({
|
||||
pipelineId,
|
||||
stageId: stage.id,
|
||||
})
|
||||
}
|
||||
disabled={removeStageMutation.isPending}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--color-text-muted)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1rem',
|
||||
padding: '0 0.25rem',
|
||||
}}
|
||||
title="Stufe entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Pipeline Card (klappbar) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function PipelineCard({ pipeline }: { pipeline: Pipeline }) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [newStageName, setNewStageName] = useState('');
|
||||
|
|
@ -17,7 +168,6 @@ function PipelineCard({ pipeline }: { pipeline: Pipeline }) {
|
|||
const [isDeleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const addStageMutation = useAddStage();
|
||||
const removeStageMutation = useRemoveStage();
|
||||
const deletePipelineMutation = useDeletePipeline();
|
||||
|
||||
const stages = [...pipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
|
@ -123,34 +273,11 @@ function PipelineCard({ pipeline }: { pipeline: Pipeline }) {
|
|||
</p>
|
||||
)}
|
||||
{stages.map((stage) => (
|
||||
<div key={stage.id} className={styles.stageItem}>
|
||||
<span className={styles.stageOrder}>{stage.sortOrder + 1}</span>
|
||||
<span
|
||||
className={styles.stageColor}
|
||||
style={{ background: stage.color }}
|
||||
/>
|
||||
<span className={styles.stageName}>{stage.name}</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
removeStageMutation.mutate({
|
||||
pipelineId: pipeline.id,
|
||||
stageId: stage.id,
|
||||
})
|
||||
}
|
||||
disabled={removeStageMutation.isPending}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--color-text-muted)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1rem',
|
||||
padding: '0 0.25rem',
|
||||
}}
|
||||
title="Stufe entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<StageRow
|
||||
key={stage.id}
|
||||
stage={stage}
|
||||
pipelineId={pipeline.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Neue Stufe hinzufügen */}
|
||||
|
|
@ -227,7 +354,7 @@ function PipelineCard({ pipeline }: { pipeline: Pipeline }) {
|
|||
marginBottom: '1.5rem',
|
||||
}}
|
||||
>
|
||||
Alle Stufen und zugehörige Deals werden ebenfalls gelöscht.
|
||||
Alle Stufen und zugehörige Vorgänge werden ebenfalls gelöscht.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -280,6 +407,10 @@ function PipelineCard({ pipeline }: { pipeline: Pipeline }) {
|
|||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Hauptseite */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function PipelinesPage() {
|
||||
const { data, isLoading, error } = usePipelines();
|
||||
const createMutation = useCreatePipeline();
|
||||
|
|
@ -471,7 +602,7 @@ export function PipelinesPage() {
|
|||
Noch keine Pipelines vorhanden
|
||||
</p>
|
||||
<p style={{ fontSize: '0.875rem' }}>
|
||||
Erstelle eine Pipeline, um Deals zu verwalten.
|
||||
Erstelle eine Pipeline, um Vorgänge zu verwalten.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ export interface UpdatePipelinePayload {
|
|||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateStagePayload {
|
||||
name?: string;
|
||||
sortOrder?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface CreateStagePayload {
|
||||
name: string;
|
||||
sortOrder?: number;
|
||||
|
|
@ -153,7 +159,7 @@ export interface Deal {
|
|||
updatedBy: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
pipeline?: { id: string; name: string };
|
||||
pipeline?: { id: string; name: string; stages?: PipelineStage[] };
|
||||
stage?: { id: string; name: string; color: string };
|
||||
contact?: {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ export function AppLayout() {
|
|||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Deals"
|
||||
title="Vorgänge"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
|
|
@ -301,7 +301,7 @@ export function AppLayout() {
|
|||
<path d="M5 5V3a2 2 0 012-2h2a2 2 0 012 2v2" />
|
||||
<path d="M1 9h14" />
|
||||
</svg>
|
||||
{!collapsed && 'Deals'}
|
||||
{!collapsed && 'Vorgänge'}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/crm/pipelines"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue