feat: wire offer→project phases/tasks, redo Offerte tab, cleanup project tabs
- importOfferIntoProject(): materializes project phases/tasks from the assigned tier's services grouped by services.fase (merge-by-title, dedup tasks) - assignOfferToProject: optional import_phases flag triggers the import - getProjectFullDetail: projectOffers/availableMicros now include macro name + tier_letter (dropdown shows "Offerta — Tier X"); availableMicros filters non-archived macros - OffersTab redone: shows macro + tier badge, "Importa fasi e task" checkbox - removed project "Preventivo" tab + deleted QuoteTab.tsx (legacy quote_items) - sidebar: Lead before Clienti - no DB migration (reuses existing tables) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,8 @@ export type ProjectOfferWithMicro = {
|
||||
micro_internal_name: string;
|
||||
micro_public_name: string;
|
||||
micro_duration_months: number;
|
||||
tier_letter: string | null;
|
||||
macro_internal_name: string;
|
||||
start_date: Date;
|
||||
accepted_total: string | null;
|
||||
created_at: Date;
|
||||
@@ -548,7 +550,14 @@ export type ProjectFullDetail = {
|
||||
activeTimerStartedAt: Date | null;
|
||||
totalTrackedSeconds: number;
|
||||
projectOffers: ProjectOfferWithMicro[];
|
||||
availableMicros: Array<{ id: string; internal_name: string; public_name: string; duration_months: number }>;
|
||||
availableMicros: Array<{
|
||||
id: string;
|
||||
internal_name: string;
|
||||
public_name: string;
|
||||
duration_months: number;
|
||||
tier_letter: string | null;
|
||||
macro_internal_name: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function getProjectFullDetail(id: string): Promise<ProjectFullDetail | null> {
|
||||
@@ -624,7 +633,7 @@ export async function getProjectFullDetail(id: string): Promise<ProjectFullDetai
|
||||
.select({ total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)` })
|
||||
.from(time_entries)
|
||||
.where(eq(time_entries.project_id, id)),
|
||||
// Query A: project offers for this project joined with micro info
|
||||
// Query A: project offers for this project joined with micro + macro info
|
||||
db
|
||||
.select({
|
||||
id: project_offers.id,
|
||||
@@ -633,24 +642,31 @@ export async function getProjectFullDetail(id: string): Promise<ProjectFullDetai
|
||||
micro_internal_name: offer_micros.internal_name,
|
||||
micro_public_name: offer_micros.public_name,
|
||||
micro_duration_months: offer_micros.duration_months,
|
||||
tier_letter: offer_micros.tier_letter,
|
||||
macro_internal_name: offer_macros.internal_name,
|
||||
start_date: project_offers.start_date,
|
||||
accepted_total: project_offers.accepted_total,
|
||||
created_at: project_offers.created_at,
|
||||
})
|
||||
.from(project_offers)
|
||||
.innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id))
|
||||
.innerJoin(offer_macros, eq(offer_micros.macro_id, offer_macros.id))
|
||||
.where(eq(project_offers.project_id, id))
|
||||
.orderBy(asc(project_offers.created_at)),
|
||||
// Query B: all active micro-offers (for the assignment dropdown)
|
||||
// Query B: all tiers of non-archived offers (for the assignment dropdown)
|
||||
db
|
||||
.select({
|
||||
id: offer_micros.id,
|
||||
internal_name: offer_micros.internal_name,
|
||||
public_name: offer_micros.public_name,
|
||||
duration_months: offer_micros.duration_months,
|
||||
tier_letter: offer_micros.tier_letter,
|
||||
macro_internal_name: offer_macros.internal_name,
|
||||
})
|
||||
.from(offer_micros)
|
||||
.orderBy(asc(offer_micros.internal_name)),
|
||||
.innerJoin(offer_macros, eq(offer_micros.macro_id, offer_macros.id))
|
||||
.where(eq(offer_macros.is_archived, false))
|
||||
.orderBy(asc(offer_macros.internal_name), asc(offer_micros.tier_letter)),
|
||||
]);
|
||||
|
||||
const allEntityIds = [id, ...taskIds, ...deliverablesRows.map((d) => d.id)];
|
||||
|
||||
Reference in New Issue
Block a user