614cf0114f
- src/lib/quote-actions.ts: createQuote server action with nanoid tokens and atomic DB transaction - src/components/admin/quotes/QuoteBuilderForm.tsx: Two-column form (left: client/offer/price inputs, right: live preview) - src/components/admin/quotes/OfferSelector.tsx: Grouped dropdown for macro/micro offer selection - src/components/admin/quotes/PriceOverrideInput.tsx: Price input with server validation feedback - src/components/admin/quotes/QuotePreview.tsx: Live preview card showing offer details and calculated total - src/app/admin/quotes/new/page.tsx: Quote builder page rendering form with data from DB - src/app/admin/quotes/new/actions.ts: Re-export of createQuote for page-level action imports - src/lib/admin-queries.ts: Added getAllOfferMacrosWithMicros() query for form population Features: - Admin selects client and offer (required fields) - Form validates on blur and displays success/error states - Server action validates data and rejects if total mismatch - On success, displays public /quote/[token] link with copy button - Quote saved as draft state with immutable accepted_total Completes Phase 9 Plan 2 Wave 1: Admin UI Layer Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import { Card } from "@/components/ui/card";
|
|
import { OfferMicro, OfferPhase } from "@/db/schema";
|
|
|
|
interface QuotePreviewProps {
|
|
offer: (OfferMicro & { phases: OfferPhase[] }) | null;
|
|
selectedTotal: number;
|
|
}
|
|
|
|
export function QuotePreview({ offer, selectedTotal }: QuotePreviewProps) {
|
|
if (!offer) {
|
|
return (
|
|
<div className="h-full flex items-center justify-center text-gray-400">
|
|
<p className="text-sm">Seleziona un'offerta per visualizzare l'anteprima</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="p-6 space-y-6">
|
|
<div className="border-b pb-6">
|
|
<h3 className="text-lg font-semibold text-gray-900">{offer.public_name}</h3>
|
|
{offer.transformation_promise && (
|
|
<p className="text-sm text-gray-600 mt-2">{offer.transformation_promise}</p>
|
|
)}
|
|
<p className="text-xs text-gray-500 mt-3">
|
|
Durata: {offer.duration_months} mese{offer.duration_months !== 1 ? "i" : ""}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<h4 className="font-medium text-gray-900">Fasi incluse</h4>
|
|
{offer.phases && offer.phases.length > 0 ? (
|
|
offer.phases.map((phase) => (
|
|
<div key={phase.id} className="space-y-2">
|
|
<p className="text-sm font-medium text-gray-800">{phase.title}</p>
|
|
{phase.description && (
|
|
<p className="text-xs text-gray-600 ml-2">{phase.description}</p>
|
|
)}
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-gray-400">Nessuna fase configurata</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="border-t pt-6 space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-600">Totale preventivo</span>
|
|
<span className="text-2xl font-bold text-gray-900">
|
|
€{selectedTotal.toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
Questo preventivo sarà visibile al cliente tramite link riservato
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|