diff --git a/apps/api/src/tenders/tenders.contracts.ts b/apps/api/src/tenders/tenders.contracts.ts index fa7064bf4..df01f8ff4 100644 --- a/apps/api/src/tenders/tenders.contracts.ts +++ b/apps/api/src/tenders/tenders.contracts.ts @@ -3,9 +3,17 @@ import { z } from "zod"; // Bornes calquees sur api_v2/routers/tenders.py::list_tenders -- valider ici // evite d'expedier une requete que l'API VigieProcure refusera de toute // facon, et donne une erreur Zod lisible cote CRM plutot qu'un 422 distant. +// +// `status` : mapping metier valide cote fiche compte (onglet "Marches +// publics") -- "En cours" = active, "Notifie" = awarded, "Prevu" = +// previsionnel, "Tous" = parametre omis. `published`/`cancelled`/`expired` +// existent cote api_v2 mais n'ont pas d'usage CRM identifie ; ne pas les +// exposer ici tant qu'un besoin ne les justifie pas. export const tendersListOuvertsInput = z.object({ cpv: z.string().trim().min(2).max(400).optional(), department: z.string().trim().min(1).max(3).optional(), + siren: z.string().trim().min(9).max(14).optional(), + status: z.enum(["active", "awarded", "previsionnel"]).optional(), q: z.string().trim().min(2).max(200).optional(), page: z.number().int().min(1).max(500).default(1), limit: z.number().int().min(1).max(100).default(20), diff --git a/apps/api/src/tenders/tenders.service.ts b/apps/api/src/tenders/tenders.service.ts index 12b04a68c..94c000f59 100644 --- a/apps/api/src/tenders/tenders.service.ts +++ b/apps/api/src/tenders/tenders.service.ts @@ -16,6 +16,8 @@ export class TendersService { return listOpenTenders({ cpv: input.cpv, department: input.department, + siren: input.siren, + status: input.status, q: input.q, page: input.page, limit: input.limit, diff --git a/apps/api/src/tenders/vigieprocure-tenders.client.ts b/apps/api/src/tenders/vigieprocure-tenders.client.ts index 2b17d37e3..a5547a2a9 100644 --- a/apps/api/src/tenders/vigieprocure-tenders.client.ts +++ b/apps/api/src/tenders/vigieprocure-tenders.client.ts @@ -87,6 +87,8 @@ export type TendersPage = { export type TendersQuery = { cpv?: string; department?: string; + siren?: string; + status?: "active" | "awarded" | "previsionnel"; q?: string; page?: number; limit?: number; @@ -100,11 +102,16 @@ export type TendersResult = | { outcome: "failed"; reason: string }; /** - * Liste les marches ouverts via l'API VigieProcure - * (`GET /api/v1/tenders?status=active`). `status` est TOUJOURS "active" -- - * ce n'est pas un parametre d'entree cote CRM, c'est le seul statut que - * cette page a vocation a montrer (marches publies et non encore - * echus, cf. api_v2/routers/tenders.py::_STATUTS). + * Liste les marches via l'API VigieProcure (`GET /api/v1/tenders`). + * + * `status` est un parametre d'entree optionnel cote CRM (mapping metier + * "En cours"=active / "Notifie"=awarded / "Prevu"=previsionnel, defini dans + * `tenders.contracts.ts`). Un appel SANS `status` transmet la requete telle + * quelle a api_v2, qui repond alors sans filtre de statut ("Tous") -- + * c'est le comportement voulu par l'onglet "Marches publics" de la fiche + * compte, PAS un defaut a "active" impose ici (cf. api_v2/routers/tenders.py + * ::_STATUTS ; `status=active` doit desormais etre demande explicitement + * par l'appelant s'il veut "En cours"). * * Pas de champ "lien vers l'avis d'origine" ici : `external_url` n'existe * QUE sur `GET /tenders/{id}` (extrait de `raw_data->>'url_avis'`), pas sur @@ -117,12 +124,16 @@ export async function listOpenTenders( if (!api) return { outcome: "not-configured" }; const target = new URL(api.url); - target.searchParams.set("status", "active"); // Omission volontaire des parametres vides : `q`/`cpv` exigent // min_length>=2 et `department` min_length>=1 cote api_v2 -- envoyer une // chaine vide donnerait un 422 sur CHAQUE chargement de page non filtre. if (query.cpv) target.searchParams.set("cpv", query.cpv); if (query.department) target.searchParams.set("department", query.department); + // api_v2 tronque au SIREN (9 premiers caracteres) quand un SIRET (14) est + // fourni -- cf. api_v2/routers/tenders.py::list_tenders. On envoie la + // valeur telle quelle, c'est la responsabilite d'api_v2, pas la notre. + if (query.siren) target.searchParams.set("siren", query.siren); + if (query.status) target.searchParams.set("status", query.status); if (query.q) target.searchParams.set("q", query.q); if (query.cursor) { target.searchParams.set("cursor", query.cursor); diff --git a/apps/api/test/vigieprocure-tenders-client.spec.ts b/apps/api/test/vigieprocure-tenders-client.spec.ts new file mode 100644 index 000000000..4fdc4c5d1 --- /dev/null +++ b/apps/api/test/vigieprocure-tenders-client.spec.ts @@ -0,0 +1,141 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import type { z } from "zod"; +import { + listOpenTenders, + vigieProcureTendersApi, +} from "../src/tenders/vigieprocure-tenders.client"; + +const realFetch = globalThis.fetch; + +let previousUrl: string | undefined; +let previousJwt: string | undefined; + +beforeEach(() => { + previousUrl = process.env.VIGIEPROCURE_API_URL; + previousJwt = process.env.VIGIEPROCURE_API_JWT; + process.env.VIGIEPROCURE_API_URL = "https://api.vigieproc.fr"; + process.env.VIGIEPROCURE_API_JWT = "test-jwt"; +}); + +afterEach(() => { + globalThis.fetch = realFetch; + if (previousUrl === undefined) delete process.env.VIGIEPROCURE_API_URL; + else process.env.VIGIEPROCURE_API_URL = previousUrl; + if (previousJwt === undefined) delete process.env.VIGIEPROCURE_API_JWT; + else process.env.VIGIEPROCURE_API_JWT = previousJwt; +}); + +type FetchStub = { calledWith: () => URL }; + +/** Stub fetch and capture the request URL it was called with. */ +function stub(status: number, body: z.core.util.JSONType): FetchStub { + let captured: URL | null = null; + globalThis.fetch = (async (input: Parameters[0]) => { + captured = input instanceof Request ? new URL(input.url) : new URL(input); + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + return { + calledWith: () => { + if (!captured) throw new Error("fetch was not called"); + return captured; + }, + }; +} + +const emptyPage = { + items: [], + count: 0, + total: 0, + total_est_plafonne: false, + page: 1, + page_size: 20, + next_cursor: null, +}; + +describe("vigieProcureTendersApi", () => { + it("returns null when the JWT is unset -- not an unauthenticated call", () => { + delete process.env.VIGIEPROCURE_API_JWT; + expect(vigieProcureTendersApi()).toBeNull(); + }); + + it("returns null when the URL is unset", () => { + delete process.env.VIGIEPROCURE_API_URL; + expect(vigieProcureTendersApi()).toBeNull(); + }); +}); + +describe("listOpenTenders", () => { + it("degrades to not-configured when the env is unset -- never crashes", async () => { + delete process.env.VIGIEPROCURE_API_URL; + delete process.env.VIGIEPROCURE_API_JWT; + const result = await listOpenTenders({}); + expect(result).toEqual({ outcome: "not-configured" }); + }); + + it("sends no status param when none is given -- 'Tous', not a hidden default to active", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({}); + expect(fetchSpy.calledWith().searchParams.has("status")).toBe(false); + }); + + it("forwards an explicit status=active ('En cours')", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({ status: "active" }); + expect(fetchSpy.calledWith().searchParams.get("status")).toBe("active"); + }); + + it("forwards status=awarded ('Notifie')", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({ status: "awarded" }); + expect(fetchSpy.calledWith().searchParams.get("status")).toBe("awarded"); + }); + + it("forwards status=previsionnel ('Prevu')", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({ status: "previsionnel" }); + expect(fetchSpy.calledWith().searchParams.get("status")).toBe( + "previsionnel", + ); + }); + + it("forwards siren untouched -- truncation to 9 digits is api_v2's job", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({ siren: "42498265000012" }); + expect(fetchSpy.calledWith().searchParams.get("siren")).toBe( + "42498265000012", + ); + }); + + it("omits siren when not given", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({}); + expect(fetchSpy.calledWith().searchParams.has("siren")).toBe(false); + }); + + it("combines siren and status together (account-tab use case)", async () => { + const fetchSpy = stub(200, emptyPage); + await listOpenTenders({ siren: "424982650", status: "awarded" }); + const params = fetchSpy.calledWith().searchParams; + expect(params.get("siren")).toBe("424982650"); + expect(params.get("status")).toBe("awarded"); + }); + + it("maps 401 to unauthorized", async () => { + stub(401, { detail: "Not authenticated" }); + const result = await listOpenTenders({}); + expect(result.outcome).toBe("unauthorized"); + }); + + it("maps a 200 body through to outcome ok", async () => { + stub(200, emptyPage); + const result = await listOpenTenders({}); + expect(result.outcome).toBe("ok"); + if (result.outcome === "ok") { + expect(result.page.items).toEqual([]); + expect(result.page.total).toBe(0); + } + }); +}); diff --git a/apps/app/app/(app)/[slug]/marches-publics/marches-publics-list.tsx b/apps/app/app/(app)/[slug]/marches-publics/marches-publics-list.tsx deleted file mode 100644 index c49cf79e6..000000000 --- a/apps/app/app/(app)/[slug]/marches-publics/marches-publics-list.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import ChevronLeft from "@carbon/icons-react/es/ChevronLeft"; -import ChevronRight from "@carbon/icons-react/es/ChevronRight"; -import Search from "@carbon/icons-react/es/Search"; -import WarningAlt from "@carbon/icons-react/es/WarningAlt"; -import { Alert, AlertDescription, AlertTitle } from "@crm/ui/components/alert"; -import { Badge } from "@crm/ui/components/badge"; -import { Button } from "@crm/ui/components/button"; -import { - Empty, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from "@crm/ui/components/empty"; -import { - InputGroup, - InputGroupAddon, - InputGroupInput, -} from "@crm/ui/components/input-group"; -import { Spinner } from "@crm/ui/components/spinner"; -import { useQuery } from "@tanstack/react-query"; -import { useQueryStates } from "nuqs"; -import { LocalDay } from "@/components/local-date-time"; -import { useTRPC } from "@/lib/trpc/client"; -import type { RouterOutputs } from "@/lib/trpc/types"; -import { - marchesPublicsParsers, - marchesPublicsSearchParams, -} from "./marches-publics-search-params"; - -type TendersResult = RouterOutputs["tenders"]["listOuverts"]; -type Tender = Extract< - TendersResult, - { outcome: "ok" } ->["page"]["items"][number]; - -const AMOUNT_FORMATTER = new Intl.NumberFormat("fr-FR", { - style: "currency", - currency: "EUR", - maximumFractionDigits: 0, -}); - -function formatAmount(amount: number | null): string { - return amount == null ? "—" : AMOUNT_FORMATTER.format(amount); -} - -export function MarchesPublicsList() { - const trpc = useTRPC(); - const [values, setValues] = useQueryStates(marchesPublicsParsers); - const input = marchesPublicsSearchParams.toInput(values); - - const result = useQuery({ - ...trpc.tenders.listOuverts.queryOptions(input), - placeholderData: (previous) => previous, - }); - - if (result.isPending) { - return ( -
- -
- ); - } - - if (result.isError) { - return ( - - - Impossible de charger les marches - {result.error.message} - - ); - } - - const data = result.data; - - if (data.outcome === "not-configured") { - return ( - - - - - - VigieProcure n'est pas configure - - VIGIEPROCURE_API_URL ou VIGIEPROCURE_API_JWT est absent de - l'environnement de crm-api. Cette page reste desactivee tant - que ces variables ne sont pas provisionnees. - - - - ); - } - - if (data.outcome === "unauthorized" || data.outcome === "failed") { - return ( - - - VigieProcure indisponible - {data.reason} - - ); - } - - const { page } = data; - - return ( -
-
- - - - - { - const q = event.target.value; - setValues((prev) => ({ ...prev, q, page: 1 })); - }} - autoComplete="off" - /> - - - { - const cpv = event.target.value; - setValues((prev) => ({ ...prev, cpv, page: 1 })); - }} - autoComplete="off" - /> - - - { - const department = event.target.value; - setValues((prev) => ({ ...prev, department, page: 1 })); - }} - autoComplete="off" - /> - - {result.isFetching && } -
- - {page.items.length === 0 ? ( - - - Aucun marche ne correspond - - Essayez d'elargir les filtres CPV, departement ou la - recherche texte. - - - - ) : ( -
- - - - - - - - - - - - {page.items.map((tender) => ( - - ))} - -
ObjetAcheteurCPVMontantDate limite
-
- )} - -
- - {page.total} - {page.totalEstPlafonne ? "+" : ""} marche - {page.total > 1 ? "s" : ""} ouvert - {page.total > 1 ? "s" : ""} - -
- - - Page {page.page} - - -
-
-
- ); -} - -function TenderRow({ tender }: { tender: Tender }) { - return ( - - - - {tender.titleDisplay ?? tender.title ?? "—"} - - - - {tender.buyerName ?? "—"} - - - {tender.cpvCode ? {tender.cpvCode} : "—"} - - - {formatAmount(tender.amountEstimated)} - - - {tender.deadlineAt ? : "—"} - - - ); -} diff --git a/apps/app/app/(app)/[slug]/marches-publics/marches-publics-search-params.ts b/apps/app/app/(app)/[slug]/marches-publics/marches-publics-search-params.ts deleted file mode 100644 index 9567529aa..000000000 --- a/apps/app/app/(app)/[slug]/marches-publics/marches-publics-search-params.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createLoader, parseAsInteger, parseAsString } from "nuqs/server"; - -// Parametres propres a cette page -- PAS `createListSearchParams` -// (`@/components/data-table/list-search-params`), qui suppose une liste -// Prisma avec offset/facettes/tri par colonne. Le feed VigieProcure est un -// proxy en lecture vers une API externe avec sa propre pagination -// (page/limit, pas de facettes) : reutiliser cette machinerie aurait force -// un desaccord de forme plutot qu'un raccourci. -export const marchesPublicsParsers = { - q: parseAsString.withDefault(""), - cpv: parseAsString.withDefault(""), - department: parseAsString.withDefault(""), - page: parseAsInteger.withDefault(1).withOptions({ history: "push" }), -}; - -type MarchesPublicsValues = { - q: string; - cpv: string; - department: string; - page: number; -}; - -const DEFAULTS: MarchesPublicsValues = { - q: "", - cpv: "", - department: "", - page: 1, -}; - -function toInput(values: MarchesPublicsValues) { - return { - q: values.q.trim() || undefined, - cpv: values.cpv.trim() || undefined, - department: values.department.trim() || undefined, - page: values.page > 0 ? values.page : 1, - limit: 20, - }; -} - -export const marchesPublicsSearchParams = { - load: createLoader(marchesPublicsParsers), - toInput, - defaultInput: () => toInput(DEFAULTS), -}; diff --git a/apps/app/app/(app)/[slug]/marches-publics/page.tsx b/apps/app/app/(app)/[slug]/marches-publics/page.tsx deleted file mode 100644 index 2ac0d4d0c..000000000 --- a/apps/app/app/(app)/[slug]/marches-publics/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { Metadata } from "next"; -import { Suspense } from "react"; -import { - PageShell, - PageShellContent, - PageShellDescription, - PageShellHeader, - PageShellHeading, - PageShellLoading, - PageShellTitle, -} from "@/components/page-shell"; -import { requireSession } from "@/lib/session"; -import { HydrateClient } from "@/lib/trpc/hydrate"; -import { getServerQueryClient, getServerTrpc } from "@/lib/trpc/server"; -import { MarchesPublicsList } from "./marches-publics-list"; -import { marchesPublicsSearchParams } from "./marches-publics-search-params"; - -export const metadata: Metadata = { - title: "Marches publics", -}; - -export default function MarchesPublicsPage({ - searchParams, -}: PageProps<"/[slug]/marches-publics">) { - return ( - - - - Marches publics - - Appels d'offres ouverts, via VigieProcure (BOAMP/TED/DECP). - - - - - - }> - - - - - ); -} - -async function MarchesPublics({ - searchParams, -}: Pick, "searchParams">) { - const [, values] = await Promise.all([ - requireSession(), - marchesPublicsSearchParams.load(searchParams), - ]); - - const trpc = getServerTrpc(); - const queryClient = getServerQueryClient(); - // Pas de throw en cas d'echec cote VigieProcure -- `listOuverts` rend un - // resultat degrade (`{ outcome: "not-configured" | ... }`), jamais une - // exception. Le prefetch reste donc sans danger pour le SSR meme si - // VIGIEPROCURE_API_JWT est absent ou si l'API distante est en panne. - await queryClient.prefetchQuery( - trpc.tenders.listOuverts.queryOptions( - marchesPublicsSearchParams.toInput(values), - ), - ); - - return ( - - - - ); -} diff --git a/apps/app/components/app-icon-rail.tsx b/apps/app/components/app-icon-rail.tsx index 8ed3f9aaf..ad3440e00 100644 --- a/apps/app/components/app-icon-rail.tsx +++ b/apps/app/components/app-icon-rail.tsx @@ -4,7 +4,6 @@ import Building from "@carbon/icons-react/es/Building"; import Close from "@carbon/icons-react/es/Close"; import Dashboard from "@carbon/icons-react/es/Dashboard"; import Partnership from "@carbon/icons-react/es/Partnership"; -import Search from "@carbon/icons-react/es/Search"; import Settings from "@carbon/icons-react/es/Settings"; import UserMultiple from "@carbon/icons-react/es/UserMultiple"; import { Button } from "@crm/ui/components/button"; @@ -58,12 +57,6 @@ const ITEMS: RailItem[] = [ match: "prefix", }, { title: "Deals", href: "/deals", icon: Partnership, match: "prefix" }, - { - title: "Marches publics", - href: "/marches-publics", - icon: Search, - match: "prefix", - }, { title: "Settings", href: "/settings", icon: Settings, match: "prefix" }, ]; diff --git a/apps/app/components/crm/company-tenders-panel.tsx b/apps/app/components/crm/company-tenders-panel.tsx new file mode 100644 index 000000000..b0f9ce4fb --- /dev/null +++ b/apps/app/components/crm/company-tenders-panel.tsx @@ -0,0 +1,260 @@ +"use client"; + +import Document from "@carbon/icons-react/es/Document"; +import WarningAlt from "@carbon/icons-react/es/WarningAlt"; +import { Alert, AlertDescription, AlertTitle } from "@crm/ui/components/alert"; +import { Badge } from "@crm/ui/components/badge"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@crm/ui/components/empty"; +import { Spinner } from "@crm/ui/components/spinner"; +import { ToggleGroup, ToggleGroupItem } from "@crm/ui/components/toggle-group"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { LocalDay } from "@/components/local-date-time"; +import { useTRPC } from "@/lib/trpc/client"; +import type { RouterOutputs } from "@/lib/trpc/types"; + +type TendersResult = RouterOutputs["tenders"]["listOuverts"]; +type Tender = Extract< + TendersResult, + { outcome: "ok" } +>["page"]["items"][number]; + +// "Tous" = pas de parametre `status` du tout (cf. tenders.contracts.ts) -- +// donc pas de valeur "tous" dans l'union transmise a l'API, uniquement dans +// ce type d'etat local. +type StatusFilter = "active" | "awarded" | "previsionnel" | "tous"; + +const STATUS_LABELS = { + active: "En cours", + awarded: "Notifie", + previsionnel: "Prevu", + tous: "Tous", +} satisfies Record; + +const STATUS_ORDER: StatusFilter[] = [ + "active", + "awarded", + "previsionnel", + "tous", +]; + +const AMOUNT_FORMATTER = new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: "EUR", + maximumFractionDigits: 0, +}); + +function formatAmount(amount: number | null): string { + return amount == null ? "—" : AMOUNT_FORMATTER.format(amount); +} + +function isStatusFilter(value: string): value is StatusFilter { + return (STATUS_ORDER as readonly string[]).includes(value); +} + +/** + * Onglet "Marches publics" de la fiche compte. `siren` vient de + * `company.siren` (resolu via `CompanySirenField` sur l'onglet Overview) -- + * sans lui, impossible de savoir quels marches appartiennent a cet + * acheteur, donc pas d'appel API. + */ +export function CompanyTendersPanel({ siren }: { siren: string | null }) { + if (!siren) { + return ( + + + + + + SIREN non renseigne + + Resolvez le SIREN de ce compte depuis l'onglet Overview pour + afficher ses marches publics. + + + + ); + } + + return ; +} + +function LoadedCompanyTendersPanel({ siren }: { siren: string }) { + const trpc = useTRPC(); + const [status, setStatus] = useState("active"); + + const result = useQuery({ + ...trpc.tenders.listOuverts.queryOptions({ + siren, + status: status === "tous" ? undefined : status, + limit: 20, + }), + placeholderData: (previous) => previous, + }); + + return ( +
+
+ { + if (next && isStatusFilter(next)) setStatus(next); + }} + aria-label="Statut des marches" + > + {STATUS_ORDER.map((value) => ( + + {STATUS_LABELS[value]} + + ))} + + {result.isFetching ? : null} +
+ + +
+ ); +} + +function TendersBody({ + isPending, + isError, + errorMessage, + data, +}: { + isPending: boolean; + isError: boolean; + errorMessage: string; + data: TendersResult | undefined; +}) { + if (isPending) { + return ( +
+ +
+ ); + } + + if (isError || !data) { + return ( + + + Impossible de charger les marches + {errorMessage} + + ); + } + + if (data.outcome === "not-configured") { + return ( + + + + + + VigieProcure n'est pas configure + + VIGIEPROCURE_API_URL ou VIGIEPROCURE_API_JWT est absent de + l'environnement de crm-api. Cet onglet reste desactive tant que + ces variables ne sont pas provisionnees. + + + + ); + } + + if (data.outcome === "unauthorized" || data.outcome === "failed") { + return ( + + + VigieProcure indisponible + {data.reason} + + ); + } + + const { page } = data; + + if (page.items.length === 0) { + return ( + + + + + + Aucun marche ne correspond + + Essayez un autre statut -- ce compte n'a peut-etre aucun marche + dans celui-ci actuellement. + + + + ); + } + + return ( +
+
+ + + + + + + + + + + {page.items.map((tender) => ( + + ))} + +
ObjetCPVMontantDate
+
+ + {page.total} + {page.totalEstPlafonne ? "+" : ""} marche + {page.total > 1 ? "s" : ""} + +
+ ); +} + +function TenderRow({ tender }: { tender: Tender }) { + // "Notifie"/"awarded" n'a pas de date limite de depot -- publishedAt reste + // le seul repere temporel pertinent une fois le marche attribue. + const date = + tender.status === "awarded" ? tender.publishedAt : tender.deadlineAt; + + return ( + + + + {tender.titleDisplay ?? tender.title ?? "—"} + + + + {tender.cpvCode ? {tender.cpvCode} : "—"} + + + {formatAmount(tender.amountEstimated)} + + {date ? : "—"} + + ); +} diff --git a/apps/app/components/crm/record-sheet/company-sheet.tsx b/apps/app/components/crm/record-sheet/company-sheet.tsx index e0af5ba94..154355d39 100644 --- a/apps/app/components/crm/record-sheet/company-sheet.tsx +++ b/apps/app/components/crm/record-sheet/company-sheet.tsx @@ -26,6 +26,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { AgentPanel } from "@/components/crm/agent-panel"; import { CompanySirenField } from "@/components/crm/company-siren-field"; +import { CompanyTendersPanel } from "@/components/crm/company-tenders-panel"; import { EnrichmentActions } from "@/components/crm/enrichment-actions"; import { EnrichmentIndicator } from "@/components/crm/enrichment-status"; import { FieldsCog, RecordFields } from "@/components/crm/fields/record-fields"; @@ -207,6 +208,12 @@ export function CompanySheet({ companyId }: { companyId: string }) { label: "Activity", content: , }, + { + value: "marches-publics", + label: "Marches publics", + content: , + keepMounted: true, + }, { value: "agent", label: "Agent", diff --git a/apps/app/components/crm/section-prefetch.ts b/apps/app/components/crm/section-prefetch.ts index 695683407..8636fa5ff 100644 --- a/apps/app/components/crm/section-prefetch.ts +++ b/apps/app/components/crm/section-prefetch.ts @@ -5,16 +5,9 @@ import { useCallback } from "react"; import { companiesSearchParams } from "@/app/(app)/[slug]/companies/companies-search-params"; import { contactsSearchParams } from "@/app/(app)/[slug]/contacts/contacts-search-params"; import { dealsSearchParams } from "@/app/(app)/[slug]/deals/deals-search-params"; -import { marchesPublicsSearchParams } from "@/app/(app)/[slug]/marches-publics/marches-publics-search-params"; import { useTRPC } from "@/lib/trpc/client"; -export type Section = - | "/" - | "/companies" - | "/contacts" - | "/deals" - | "/marches-publics" - | "/settings"; +export type Section = "/" | "/companies" | "/contacts" | "/deals" | "/settings"; export function usePrefetchSection(): (section: string) => void { const trpc = useTRPC(); @@ -47,13 +40,6 @@ export function usePrefetchSection(): (section: string) => void { trpc.deals.list.queryOptions(dealsSearchParams.defaultInput()), ); return; - case "/marches-publics": - void queryClient.prefetchQuery( - trpc.tenders.listOuverts.queryOptions( - marchesPublicsSearchParams.defaultInput(), - ), - ); - return; default: return; }