feat: offer badges, unified dashboard, income forecast, Pipedrive-style follow-up
- Client detail: category + tier badges on active offers - Dashboard: remove top KPI cards + recent activity; fold current KPIs into bottom MetricCard style; add 12-month income forecast chart - Forecast query: branch by offer_type (retainer = monthly fee, una_tantum = spread over duration), filter archived projects - Payments: set the month a payment was collected (paid_at) from PaymentsTab - Redesign FollowUpWidget in clean Pipedrive style (brand green, no orange) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -289,6 +289,25 @@ export async function updatePaymentStatus(paymentId: string, id: string, status:
|
||||
revalidatePath(path);
|
||||
}
|
||||
|
||||
// Imposta il mese in cui un pagamento è stato incassato (formato "YYYY-MM").
|
||||
// Mappa al primo giorno del mese (mezzogiorno UTC per evitare drift di fuso) e
|
||||
// porta lo stato a "saldato" così l'incasso viene attribuito a quel mese nelle analytics.
|
||||
export async function setPaymentPaidAt(paymentId: string, id: string, monthStr: string) {
|
||||
await requireAdmin();
|
||||
const m = /^(\d{4})-(\d{2})$/.exec(monthStr);
|
||||
if (!m) throw new Error("Mese non valido");
|
||||
const year = parseInt(m[1], 10);
|
||||
const month = parseInt(m[2], 10);
|
||||
if (month < 1 || month > 12) throw new Error("Mese non valido");
|
||||
const paid_at = new Date(Date.UTC(year, month - 1, 1, 12, 0, 0));
|
||||
await db
|
||||
.update(payments)
|
||||
.set({ paid_at, status: "saldato" })
|
||||
.where(eq(payments.id, paymentId));
|
||||
const { path } = await resolveEntity(id);
|
||||
revalidatePath(path);
|
||||
}
|
||||
|
||||
// Rescales payment amounts when the total changes.
|
||||
// If the payment has a `percent` field, use it (new plan rows).
|
||||
// Legacy rows (percent null) fall back to equal split across all rows.
|
||||
|
||||
Reference in New Issue
Block a user