feat: status selectors update

This commit is contained in:
ivannoskov
2026-02-04 12:16:34 +03:00
parent e27a171c63
commit f7597270e5
4 changed files with 354 additions and 94 deletions

View File

@@ -80,6 +80,8 @@ import {
import { Checkbox } from "@/components/ui/checkbox";
import { ColumnSelectorDialog } from "@/components/column-selector-dialog";
import { CreativeSelectDialog } from "@/components/creative-select-dialog";
import { PlacementStatusSelector } from "@/components/placement-status-selector";
import { PlacementPostStatusSelector } from "@/components/placement-post-status-selector";
import { cn } from "@/lib/utils";
import type {
Project,
@@ -98,10 +100,12 @@ import {
PLACEMENT_DEAL_STATUSES,
PLACEMENT_POST_STATUSES,
canEditPostStatus,
getPlacementStatusColor,
} from "@/lib/config/placement-columns";
type PlacementInlineDraft = {
status?: PlacementStatus;
post_status?: PlacementPostStatus | null;
comment?: string | null;
creative_id?: string | null;
creative_name?: string | null;
@@ -114,7 +118,7 @@ type PlacementInlineDraft = {
cpm?: number | null;
};
type EditableField = "status" | "placement_at" | "payment_at" | "cost" | "cost_before_bargain" | "format" | "comment" | "cost_format" | "cpm" | "creative_id" | "creative_name";
type EditableField = "status" | "post_status" | "placement_at" | "payment_at" | "cost" | "cost_before_bargain" | "format" | "comment" | "cost_format" | "cpm" | "creative_id" | "creative_name";
const FIXED_COST_TYPE: CostType = "fixed";
@@ -262,25 +266,6 @@ function CellRenderer({
}: CellRendererProps) {
const isEditing = editingCell?.placementId === placement.id;
const getStatusColor = (status: PlacementStatus): string => {
switch (status) {
case "Оплачено":
return "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800";
case "Согласование условий":
case "Ждём ответа":
return "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800";
case "Отмена":
case "Не подходит цена":
case "Неактуально":
case "Не отвечает":
return "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800";
case "Оплатить":
return "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800";
default:
return "bg-muted text-muted-foreground border-border";
}
};
const formatCurrency = (value: number): string => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
@@ -358,43 +343,14 @@ function CellRenderer({
case "status_deal":
return (
<td key={column.id} className="py-2 px-3">
{isEditing && editingCell?.field === "status" ? (
<select
aria-label="Статус размещения"
className={cn(
"h-7 w-full rounded-md border border-input bg-background px-2 text-xs font-medium shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
getStatusColor(placement.status)
)}
value={placement.status}
onChange={(e) =>
handleDraftChange(placement.id, {
status: e.target.value as PlacementStatus,
})
}
onBlur={() => setEditingCell(null)}
>
{PLACEMENT_DEAL_STATUSES.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
) : (
<button
type="button"
disabled={!canEdit}
className={cn(
"group h-7 w-full rounded-md border px-2 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-default flex items-center justify-between gap-2",
getStatusColor(placement.status)
)}
onClick={() =>
canEdit && setEditingCell({ placementId: placement.id, field: "status" })
}
>
<span>{placement.status}</span>
<Pencil className="h-3 w-3 opacity-0 group-hover:opacity-70 transition-opacity" />
</button>
)}
<PlacementStatusSelector
status={placement.status}
isEditing={isEditing && editingCell?.field === "status"}
canEdit={canEdit}
onEdit={() => canEdit && setEditingCell({ placementId: placement.id, field: "status" })}
onChange={(s) => handleDraftChange(placement.id, { status: s })}
onCloseEdit={() => setEditingCell(null)}
/>
</td>
);
@@ -402,13 +358,14 @@ function CellRenderer({
const postStatus = placement.placement_post?.status;
return (
<td key={column.id} className="py-2 px-3">
{postStatus ? (
<span className="text-xs px-2 py-1 rounded border inline-block">
{postStatus}
</span>
) : (
<span className="text-muted-foreground"></span>
)}
<PlacementPostStatusSelector
status={postStatus ?? null}
isEditing={isEditing && editingCell?.field === "post_status"}
canEdit={canEdit}
onEdit={() => canEdit && setEditingCell({ placementId: placement.id, field: "post_status" })}
onChange={(s) => handleDraftChange(placement.id, { post_status: s })}
onCloseEdit={() => setEditingCell(null)}
/>
</td>
);
@@ -1070,36 +1027,36 @@ function ChannelGroup({
? (effectiveCost / viewPlacement.placement_post.views_count) * 1000
: null;
return (
<tr
key={placement.id}
data-placement-id={placement.id}
className="border-b last:border-0 hover:bg-muted/30"
>
{ALL_COLUMNS.filter((c) => visibleColumns.has(c.id)).map((column) => (
<CellRenderer
key={column.id}
column={column}
placement={viewPlacement}
idx={idx}
channelNumber={channelNumber}
canEdit={canEdit}
editingCell={editingCell}
setEditingCell={setEditingCell}
handleDraftChange={handleDraftChange}
effectiveCost={effectiveCost}
cpf={cpf}
cpm={cpm}
selectedPlacementIds={selectedPlacementIds}
onTogglePlacement={onTogglePlacement}
onOpenCreativeSelect={onOpenCreativeSelect}
onClearCreative={onClearCreative}
onOpenCreativeSend={onOpenCreativeSend}
/>
))}
</tr>
);
})}
return (
<tr
key={placement.id}
data-placement-id={placement.id}
className="border-b last:border-0 hover:bg-muted/30"
>
{ALL_COLUMNS.filter((c) => visibleColumns.has(c.id)).map((column) => (
<CellRenderer
key={column.id}
column={column}
placement={viewPlacement}
idx={idx}
channelNumber={channelNumber}
canEdit={canEdit}
editingCell={editingCell}
setEditingCell={setEditingCell}
handleDraftChange={handleDraftChange}
effectiveCost={effectiveCost}
cpf={cpf}
cpm={cpm}
selectedPlacementIds={selectedPlacementIds}
onTogglePlacement={onTogglePlacement}
onOpenCreativeSelect={onOpenCreativeSelect}
onClearCreative={onClearCreative}
onOpenCreativeSend={onOpenCreativeSend}
/>
))}
</tr>
);
})}
</tbody>
</table>
</div>
@@ -1313,6 +1270,10 @@ export default function PurchasePlanDetailPage() {
const nextStatus = draft.status ?? placement.status;
const nextComment = draft.comment !== undefined ? draft.comment : placement.comment;
const nextPlacementPost = draft.post_status !== null && draft.post_status !== undefined && placement.placement_post
? { ...placement.placement_post, status: draft.post_status }
: placement.placement_post;
const nextDetails = {
...(placement.details || {
placement_at: null,
@@ -1357,6 +1318,7 @@ export default function PurchasePlanDetailPage() {
return {
...placement,
status: nextStatus,
placement_post: nextPlacementPost,
comment: nextComment,
creative_id: draft.creative_id ?? placement.creative_id,
creative_name: draft.creative_name ?? placement.creative_name,