Simpl-Resultat/src/components/adjustments/AdjustmentDetailPanel.tsx
le king fu 155722775a
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
fix(dialogs): wait for the confirmation before deleting or removing the key
On desktop, tauri-plugin-dialog 2.6.0 replaces window.confirm with an async
function: confirm(...) returned a Promise, always truthy, so every guard
`if (!confirm(...)) return;` let the action run before the user answered.
Deleting a secondary profile removed its database even on "Cancel".

- confirmAction() (src/services/confirmService.ts) calls the plugin's ask()
  with a warning icon and buttons labelled in the app language
  (common.confirm / common.cancel); a failed call resolves false and is
  logged (fail closed). @tauri-apps/plugin-dialog 2.6.0 added, the crate's
  version.
- The six sites await it: profile, category, adjustment, budget template and
  Balance category deletions, and both confirmations of the license key
  removal (performRemoveLicense takes an async confirm).
- native-dialog-guard.test.ts fails on any native confirm/alert in src/ and
  on any confirmation not awaited (TypeScript does not flag `!promise`).
- Tests: confirmService, the deletion of a profile in ProfileFormModal, and
  the license removal tests with an async confirm (test: reproduces #421:
  six of them were red on the old code).

Refs #421

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:44:02 -04:00

188 lines
6.7 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Pencil, RefreshCw } from "lucide-react";
import type { Adjustment, Category } from "../../shared/types";
import type { AdjustmentEntryWithCategory } from "../../services/adjustmentService";
import type { AdjustmentFormData, EntryFormData } from "../../hooks/useAdjustments";
import { confirmAction } from "../../services/confirmService";
import AdjustmentForm from "./AdjustmentForm";
interface Props {
selectedAdjustment: Adjustment | null;
entries: AdjustmentEntryWithCategory[];
categories: Category[];
editingAdjustment: AdjustmentFormData | null;
editingEntries: EntryFormData[];
isCreating: boolean;
isSaving: boolean;
onStartEditing: () => void;
onCancelEditing: () => void;
onSave: (data: AdjustmentFormData, entries: EntryFormData[]) => void;
onDelete: (id: number) => void;
}
export default function AdjustmentDetailPanel({
selectedAdjustment,
entries,
categories,
editingAdjustment,
editingEntries,
isCreating,
isSaving,
onStartEditing,
onCancelEditing,
onSave,
onDelete,
}: Props) {
const { t } = useTranslation();
const handleDelete = async () => {
if (!selectedAdjustment) return;
if (!(await confirmAction(t("adjustments.deleteConfirm")))) return;
onDelete(selectedAdjustment.id);
};
// No selection and not creating
if (!selectedAdjustment && !isCreating) {
return (
<div className="flex-1 flex items-center justify-center bg-[var(--card)] rounded-xl border border-[var(--border)] p-8">
<p className="text-[var(--muted-foreground)]">{t("adjustments.selectAdjustment")}</p>
</div>
);
}
// Creating new
if (isCreating && editingAdjustment) {
return (
<div className="flex-1 bg-[var(--card)] rounded-xl border border-[var(--border)] p-6 overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">{t("adjustments.newAdjustment")}</h2>
<AdjustmentForm
initialData={editingAdjustment}
initialEntries={editingEntries}
categories={categories}
isCreating
isSaving={isSaving}
onSave={onSave}
onCancel={onCancelEditing}
/>
</div>
);
}
if (!selectedAdjustment) return null;
// Editing existing
if (editingAdjustment) {
return (
<div className="flex-1 bg-[var(--card)] rounded-xl border border-[var(--border)] p-6 overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">{t("adjustments.editAdjustment")}</h2>
<AdjustmentForm
initialData={editingAdjustment}
initialEntries={editingEntries}
categories={categories}
isCreating={false}
isSaving={isSaving}
onSave={onSave}
onCancel={onCancelEditing}
onDelete={handleDelete}
/>
</div>
);
}
// Read-only view
const total = entries.reduce((sum, e) => sum + e.amount, 0);
return (
<div className="flex-1 bg-[var(--card)] rounded-xl border border-[var(--border)] p-6 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold">{selectedAdjustment.name}</h2>
{selectedAdjustment.is_recurring && (
<RefreshCw size={14} className="text-[var(--primary)]" />
)}
</div>
<button
onClick={onStartEditing}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg border border-[var(--border)] text-sm hover:bg-[var(--muted)]"
>
<Pencil size={14} />
{t("common.edit")}
</button>
</div>
<div className="grid grid-cols-2 gap-4 mb-6 text-sm">
<div>
<span className="text-[var(--muted-foreground)]">{t("adjustments.date")}</span>
<p className="font-medium">{selectedAdjustment.date}</p>
</div>
<div>
<span className="text-[var(--muted-foreground)]">{t("adjustments.recurring")}</span>
<p className="font-medium">{selectedAdjustment.is_recurring ? "Yes" : "No"}</p>
</div>
{selectedAdjustment.description && (
<div className="col-span-2">
<span className="text-[var(--muted-foreground)]">{t("adjustments.description")}</span>
<p className="font-medium">{selectedAdjustment.description}</p>
</div>
)}
</div>
{/* Entries table */}
<div className="border-t border-[var(--border)] pt-4">
<h3 className="text-sm font-semibold mb-3">{t("adjustments.entries")}</h3>
{entries.length === 0 ? (
<p className="text-xs text-[var(--muted-foreground)]">{t("adjustments.noEntries")}</p>
) : (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-[var(--muted-foreground)] text-xs">
<th className="pb-2 font-medium">{t("adjustments.category")}</th>
<th className="pb-2 font-medium">{t("adjustments.description")}</th>
<th className="pb-2 font-medium text-right">{t("adjustments.amount")}</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr key={entry.id} className="border-t border-[var(--border)]/50">
<td className="py-2">
<div className="flex items-center gap-2">
<span
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
style={{ backgroundColor: entry.category_color }}
/>
{entry.category_name}
</div>
</td>
<td className="py-2 text-[var(--muted-foreground)]">
{entry.description || "—"}
</td>
<td
className={`py-2 text-right font-medium ${
entry.amount >= 0 ? "text-[var(--positive)]" : "text-[var(--negative)]"
}`}
>
{entry.amount >= 0 ? "+" : ""}
{entry.amount.toFixed(2)}
</td>
</tr>
))}
<tr className="border-t border-[var(--border)]">
<td className="py-2 font-semibold" colSpan={2}>
{t("adjustments.total")}
</td>
<td
className={`py-2 text-right font-semibold ${
total >= 0 ? "text-[var(--positive)]" : "text-[var(--negative)]"
}`}
>
{total >= 0 ? "+" : ""}
{total.toFixed(2)}
</td>
</tr>
</tbody>
</table>
)}
</div>
</div>
);
}