diff --git a/bun.lockb b/bun.lockb index 322ddc86..9a3a77d1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/hyyypertool.code-workspace b/hyyypertool.code-workspace index daba03cf..8e0385c6 100644 --- a/hyyypertool.code-workspace +++ b/hyyypertool.code-workspace @@ -213,6 +213,17 @@ "problemMatcher": ["$tsc-watch"], "runOptions": { "instanceLimit": 1 }, }, + { + "label": "🧪 Update Test Snapshots : Current file", + "command": "bun test --update-snapshots ${fileDirname}/${fileBasename}", + "type": "shell", + "group": "build", + "options": { + "cwd": "${workspaceFolder:root}", + }, + "problemMatcher": ["$tsc-watch"], + "runOptions": { "instanceLimit": 1 }, + }, { "label": "🧪 Watch Test", "command": "bun test --watch", diff --git a/packages/~/app/core/package.json b/packages/~/app/core/package.json index 4841b978..c7afda23 100644 --- a/packages/~/app/core/package.json +++ b/packages/~/app/core/package.json @@ -5,23 +5,17 @@ "type": "module", "exports": { ".": "./src/index.ts", - "./config": { - "default": "./src/config/index.ts" + "./config/*": { + "default": "./src/config/*.ts" }, - "./error": { - "default": "./src/error/index.ts" + "./date/*": { + "default": "./src/date/*.ts" }, - "./html": { - "default": "./src/html/index.ts" - }, - "./htmx": { - "default": "./src/htmx/index.ts" - }, - "./schema": { - "default": "./src/schema/index.ts" + "./schema/*": { + "default": "./src/schema/*.ts" }, "./*": { - "default": "./src/*.ts" + "default": "./src/*/index.ts" } }, "scripts": { @@ -30,6 +24,7 @@ "dependencies": { "modern-errors": "7.0.1", "ts-pattern": "5.4.0", + "type-fest": "4.26.1", "zod": "3.23.8" }, "devDependencies": { diff --git a/packages/~/app/core/src/types/index.ts b/packages/~/app/core/src/types/index.ts new file mode 100644 index 00000000..311e4809 --- /dev/null +++ b/packages/~/app/core/src/types/index.ts @@ -0,0 +1,3 @@ +// + +export type { SetOptional, Simplify, SimplifyDeep } from "type-fest"; diff --git a/packages/~/app/ui/src/testing/index.ts b/packages/~/app/ui/src/testing/index.ts index 3ee05622..ed9687cb 100644 --- a/packages/~/app/ui/src/testing/index.ts +++ b/packages/~/app/ui/src/testing/index.ts @@ -2,28 +2,35 @@ import type { Child } from "hono/jsx"; import { renderToReadableStream } from "hono/jsx/dom/server"; -import { format } from "prettier"; +import { format, type Options } from "prettier"; // -export async function renderHTML(element: Child) { - const textDecoder = new TextDecoder(); - const getStringFromStream = async ( - stream: ReadableStream, - ): Promise => { - const reader = stream.getReader(); - let str = ""; - for (;;) { - const { done, value } = await reader.read(); - if (done) { - break; +export const render_html = PrettyRenderer({ parser: "html" }); +export const render_md = PrettyRenderer({ parser: "mdx" }); + +// + +export function PrettyRenderer(options: Options) { + return async function render_formated(element: Child) { + const textDecoder = new TextDecoder(); + const getStringFromStream = async ( + stream: ReadableStream, + ): Promise => { + const reader = stream.getReader(); + let str = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) { + break; + } + str += textDecoder.decode(value); } - str += textDecoder.decode(value); - } - return str; + return str; + }; + return format( + await getStringFromStream(await renderToReadableStream(element)), + options, + ); }; - return format( - await getStringFromStream(await renderToReadableStream(element)), - { parser: "html" }, - ); } diff --git a/packages/~/app/urls/package.json b/packages/~/app/urls/package.json index 849c0cc3..dd690010 100644 --- a/packages/~/app/urls/package.json +++ b/packages/~/app/urls/package.json @@ -16,8 +16,7 @@ "dependencies": { "consola": "3.2.3", "hono": "4.6.3", - "static-path": "0.0.4", - "type-fest": "4.26.1" + "static-path": "0.0.4" }, "devDependencies": { "@~/config.typescript": "workspace:*" diff --git a/packages/~/app/urls/src/hx_urls.ts b/packages/~/app/urls/src/hx_urls.ts index 5638d368..8aa36858 100644 --- a/packages/~/app/urls/src/hx_urls.ts +++ b/packages/~/app/urls/src/hx_urls.ts @@ -1,10 +1,10 @@ // +import type { SetOptional } from "@~/app.core/types"; import type { Hono, HonoRequest, Schema } from "hono"; import { hc } from "hono/client"; import type { Endpoint } from "hono/types"; import type { HasRequiredKeys, UnionToIntersection } from "hono/utils/types"; -import type { SetOptional } from "type-fest"; import type { Router } from "./pattern"; // diff --git a/packages/~/moderations/api/package.json b/packages/~/moderations/api/package.json index f0521f7b..61721d20 100644 --- a/packages/~/moderations/api/package.json +++ b/packages/~/moderations/api/package.json @@ -29,6 +29,7 @@ "@~/organizations.repository": "workspace:*", "@~/organizations.ui": "workspace:*", "@~/users.lib": "workspace:*", + "@~/users.ui": "workspace:*", "@~/zammad.lib": "workspace:*", "await-to-js": "3.0.0", "drizzle-orm": "0.33.0", diff --git a/packages/~/moderations/api/src/:id/$procedures/rejected.ts b/packages/~/moderations/api/src/:id/$procedures/rejected.ts index 00ca91a8..f219966f 100644 --- a/packages/~/moderations/api/src/:id/$procedures/rejected.ts +++ b/packages/~/moderations/api/src/:id/$procedures/rejected.ts @@ -4,11 +4,9 @@ import { zValidator } from "@hono/zod-validator"; import type { Htmx_Header } from "@~/app.core/htmx"; import { Entity_Schema } from "@~/app.core/schema"; import { set_crisp_config } from "@~/crisp.middleware"; -import { - RejectedMessage_Schema, - type RejectedModeration_Context, -} from "@~/moderations.lib/context/rejected"; +import { type RejectedModeration_Context } from "@~/moderations.lib/context/rejected"; import { MODERATION_EVENTS } from "@~/moderations.lib/event"; +import { reject_form_schema } from "@~/moderations.lib/schema/rejected.form"; import { mark_moderation_as } from "@~/moderations.lib/usecase/mark_moderation_as"; import { send_rejected_message_to_user } from "@~/moderations.lib/usecase/send_rejected_message_to_user"; import { get_moderation } from "@~/moderations.repository/get_moderation"; @@ -21,7 +19,7 @@ export default new Hono().patch( "/", set_crisp_config(), zValidator("param", Entity_Schema), - zValidator("form", RejectedMessage_Schema), + zValidator("form", reject_form_schema), async function PATH({ text, req, diff --git a/packages/~/moderations/api/src/:id/$procedures/validate.e2e.test.ts b/packages/~/moderations/api/src/:id/$procedures/validate.e2e.test.ts index fb1b0ef3..1af90a13 100644 --- a/packages/~/moderations/api/src/:id/$procedures/validate.e2e.test.ts +++ b/packages/~/moderations/api/src/:id/$procedures/validate.e2e.test.ts @@ -5,6 +5,7 @@ import { set_moncomptepro_pg } from "@~/app.middleware/set_moncomptepro_pg"; import { set_nonce } from "@~/app.middleware/set_nonce"; import { set_userinfo } from "@~/app.middleware/set_userinfo"; import { anais_tailhade } from "@~/app.middleware/set_userinfo#fixture"; +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; import { create_adora_pony_moderation, create_adora_pony_user, @@ -26,7 +27,7 @@ import { test, } from "bun:test"; import { Hono } from "hono"; -import app, { FORM_SCHEMA } from "./validate"; +import app from "./validate"; // @@ -45,7 +46,10 @@ test("GET /moderation/:id/$procedures/validate { add_domain: true, add_member: A const body = new FormData(); body.append("add_domain", "true"); - body.append("add_member", FORM_SCHEMA.shape.add_member.Enum.AS_INTERNAL); + body.append( + "add_member", + validate_form_schema.shape.add_member.enum.AS_INTERNAL, + ); const response = await new Hono() .use(set_config({})) .use(set_moncomptepro_pg(pg)) @@ -107,7 +111,10 @@ test("GET /moderation/:id/$procedures/validate { add_domain: false, add_member: const body = new FormData(); body.append("add_domain", "false"); - body.append("add_member", FORM_SCHEMA.shape.add_member.Enum.AS_EXTERNAL); + body.append( + "add_member", + validate_form_schema.shape.add_member.enum.AS_EXTERNAL, + ); const response = await new Hono() .use(set_config({})) .use(set_moncomptepro_pg(pg)) diff --git a/packages/~/moderations/api/src/:id/$procedures/validate.ts b/packages/~/moderations/api/src/:id/$procedures/validate.ts index 4657b803..51196358 100644 --- a/packages/~/moderations/api/src/:id/$procedures/validate.ts +++ b/packages/~/moderations/api/src/:id/$procedures/validate.ts @@ -4,10 +4,10 @@ import { zValidator } from "@hono/zod-validator"; import { HTTPError } from "@~/app.core/error"; import type { Htmx_Header } from "@~/app.core/htmx"; import { Entity_Schema } from "@~/app.core/schema"; -import { z_coerce_boolean } from "@~/app.core/schema/z_coerce_boolean"; import { z_email_domain } from "@~/app.core/schema/z_email_domain"; import type { App_Context } from "@~/app.middleware/context"; import { MODERATION_EVENTS } from "@~/moderations.lib/event"; +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; import { mark_moderation_as } from "@~/moderations.lib/usecase/mark_moderation_as"; import { MemberJoinOrganization } from "@~/moderations.lib/usecase/member_join_organization"; import { GetModerationById } from "@~/moderations.repository"; @@ -25,22 +25,13 @@ import consola from "consola"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; import { P, match } from "ts-pattern"; -import { z } from "zod"; - -// - -export const FORM_SCHEMA = z.object({ - add_domain: z.string().default("false").pipe(z_coerce_boolean), - add_member: z.enum(["AS_INTERNAL", "AS_EXTERNAL"]), - send_notitfication: z.string().default("false").pipe(z_coerce_boolean), -}); // export default new Hono().patch( "/", zValidator("param", Entity_Schema), - zValidator("form", FORM_SCHEMA), + zValidator("form", validate_form_schema), async function PATCH({ text, req, diff --git a/packages/~/moderations/api/src/:id/Actions.tsx b/packages/~/moderations/api/src/:id/Actions.tsx deleted file mode 100644 index 4adf6ddc..00000000 --- a/packages/~/moderations/api/src/:id/Actions.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// - -import { button } from "@~/app.ui/button"; -import { hx_urls } from "@~/app.urls"; -import { MessageInfo } from "@~/moderations.ui/MessageInfo"; -import { usePageRequestContext } from "./context"; -import { Desicison } from "./Desicison"; -import { Member_Invalid } from "./Member_Invalid"; -import { Member_Valid } from "./Member_Valid"; - -// - -export async function Moderation_Actions() { - const { - var: { moderation }, - } = usePageRequestContext(); - - const hx_moderation_reprocess_props = await hx_urls.moderations[ - ":id" - ].$procedures.reprocess.$patch({ - param: { id: moderation.id.toString() }, - }); - - return ( -
-

- Actions de modération beta{" "} -

- - - -
- {moderation.moderated_at ? ( - - ) : ( - <> - - - - - )} -
- ); -} diff --git a/packages/~/moderations/api/src/:id/Desicison_Context.tsx b/packages/~/moderations/api/src/:id/Desicison_Context.tsx deleted file mode 100644 index d4e9683d..00000000 --- a/packages/~/moderations/api/src/:id/Desicison_Context.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// - -import { hyper_ref } from "@~/app.core/html"; -import { createContext } from "hono/jsx"; - -// - -export const Desicison_Context = createContext({ - $accept: hyper_ref(), - $add_as_external_member: hyper_ref(), - $add_as_internal_member: hyper_ref(), - $add_domain: hyper_ref(), - $decision_form: hyper_ref(), - $destination: hyper_ref(), - $message: hyper_ref(), - $object: hyper_ref(), - $reject: hyper_ref(), - $select: hyper_ref(), - $send_notification: hyper_ref(), -}); diff --git a/packages/~/moderations/api/src/:id/Member_Valid.tsx b/packages/~/moderations/api/src/:id/Member_Valid.tsx deleted file mode 100644 index c3e0f277..00000000 --- a/packages/~/moderations/api/src/:id/Member_Valid.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// - -import { Htmx_Events, hx_include } from "@~/app.core/htmx"; -import { button } from "@~/app.ui/button"; -import { fieldset } from "@~/app.ui/form"; -import { hx_urls } from "@~/app.urls"; -import { Moderation_Type_Schema } from "@~/moderations.lib/Moderation_Type"; -import { useContext } from "hono/jsx"; -import { FORM_SCHEMA } from "./$procedures/validate"; -import { Desicison_Context } from "./Desicison_Context"; -import { usePageRequestContext } from "./context"; - -// - -export async function Member_Valid() { - const { $accept, $add_domain, $decision_form } = - useContext(Desicison_Context); - const { - var: { moderation }, - } = usePageRequestContext(); - const { base, element } = fieldset(); - - return ( - - ); -} - -function SendNotification() { - const { $send_notification } = useContext(Desicison_Context); - const { - var: { moderation }, - } = usePageRequestContext(); - const { - user: { email }, - } = moderation; - - return ( -
- - -
- ); -} - -function AddDomain() { - const { $add_domain } = useContext(Desicison_Context); - const { - var: { domain }, - } = usePageRequestContext(); - - return ( -
- - -
- ); -} - -function AddAsMemberInternal() { - const { $add_as_internal_member } = useContext(Desicison_Context); - const { - var: { - moderation: { - user: { given_name }, - }, - organization_member, - }, - } = usePageRequestContext(); - const is_already_internal_member = organization_member - ? organization_member.is_external === false - : true; - return ( -
- - -
- ); -} - -function AddAsMemberExternal() { - const { $add_as_external_member } = useContext(Desicison_Context); - const { - var: { - moderation: { - user: { given_name }, - }, - organization_member, - }, - } = usePageRequestContext(); - const is_already_external_member = organization_member?.is_external === true; - return ( -
- - -
- ); -} diff --git a/packages/~/moderations/api/src/:id/Organizations_Of_User_Table.tsx b/packages/~/moderations/api/src/:id/Organizations_Of_User_Table.tsx deleted file mode 100644 index 83ece150..00000000 --- a/packages/~/moderations/api/src/:id/Organizations_Of_User_Table.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// - -import { hx_trigger_from_body } from "@~/app.core/htmx"; -import { Loader } from "@~/app.ui/loader/Loader"; -import { hx_urls } from "@~/app.urls"; -import { ORGANISATION_EVENTS } from "@~/organizations.lib/event"; -import { usePageRequestContext } from "./context"; - -// - -export async function Organizations_Of_User_Table() { - const { - var: { moderation }, - } = usePageRequestContext(); - - return ( -
-

- Organisations de {moderation.user.given_name}{" "} - {moderation.user.family_name} -

- -
-
- -
-
-
- ); -} diff --git a/packages/~/moderations/api/src/:id/context.ts b/packages/~/moderations/api/src/:id/context.ts index 4c45d461..c998ce4e 100644 --- a/packages/~/moderations/api/src/:id/context.ts +++ b/packages/~/moderations/api/src/:id/context.ts @@ -13,13 +13,6 @@ import { useRequestContext } from "hono/jsx-renderer"; // -export const RESPONSE_MESSAGE_SELECT_ID = "response-message"; -export const RESPONSE_TEXTAREA_ID = "response"; -export const EMAIL_SUBJECT_INPUT_ID = "mail-subject"; -export const EMAIL_TO_INPUT_ID = "mail-to"; - -// - export interface ModerationContext extends Env { Variables: { moderation: get_moderation_dto; diff --git a/packages/~/moderations/api/src/:id/page.tsx b/packages/~/moderations/api/src/:id/page.tsx index f6c1152d..e68b3e6d 100644 --- a/packages/~/moderations/api/src/:id/page.tsx +++ b/packages/~/moderations/api/src/:id/page.tsx @@ -4,24 +4,33 @@ import { hx_trigger_from_body } from "@~/app.core/htmx"; import { button } from "@~/app.ui/button"; import { hx_urls, urls } from "@~/app.urls"; import { MODERATION_EVENTS } from "@~/moderations.lib/event"; +import { IsUserExternalMember } from "@~/moderations.lib/usecase/IsUserExternalMember"; +import { Actions } from "@~/moderations.ui/Actions"; +import { DomainsByOrganization } from "@~/moderations.ui/DomainsByOrganization"; import { Header } from "@~/moderations.ui/Header"; +import { OrganizationsByUser } from "@~/moderations.ui/OrganizationsByUser"; +import { UsersByOrganization } from "@~/moderations.ui/UsersByOrganization"; import { About as About_Organization } from "@~/organizations.ui/info/About"; import { Investigation as Investigation_Organization } from "@~/organizations.ui/info/Investigation"; +import { CountUserMemberships } from "@~/users.lib/usecase/CountUserMemberships"; +import { SuggestSameUserEmails } from "@~/users.lib/usecase/SuggestSameUserEmails"; +import { About as About_User } from "@~/users.ui/About"; +import { Investigation as Investigation_User } from "@~/users.ui/Investigation"; import { getContext } from "hono/context-storage"; -import { About_User, Investigation_User } from "./About_User"; -import { Moderation_Actions } from "./Actions"; -import { Domain_Organization } from "./Domain_Organization"; -import { Members_Of_Organization_Table } from "./Members_Of_Organization_Table"; -import { Moderation_Exchanges } from "./Moderation_Exchanges"; -import { Organizations_Of_User_Table } from "./Organizations_Of_User_Table"; import { type ModerationContext, usePageRequestContext } from "./context"; +import { Moderation_Exchanges } from "./Moderation_Exchanges"; // export default async function Moderation_Page() { const { moderation } = getContext().var; const { - var: { organization_fiche }, + var: { + moncomptepro_pg, + organization_fiche, + query_domain_count, + query_organization_members_count, + }, } = usePageRequestContext(); return ( @@ -59,7 +68,7 @@ export default async function Moderation_Page() {
- +

- +

- - +
- +
- +
- +
diff --git a/packages/~/moderations/api/src/:id/responses/__snapshots__/already_signed.test.tsx.snap b/packages/~/moderations/api/src/:id/responses/__snapshots__/already_signed.test.tsx.snap deleted file mode 100644 index 44fcb1ba..00000000 --- a/packages/~/moderations/api/src/:id/responses/__snapshots__/already_signed.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`returns all members 1`] = ` -"Bonjour, - -Votre demande pour rejoindre l'organisation « 🦄 » a été prise en compte sur https://app.moncomptepro.beta.gouv.fr. - -Vous possédez déjà un compte MonComptePro : - -- adora.pony@unicorn.xyz -- pink.diamond@unicorn.xyz -- red.diamond@unicorn.xyz - -Merci de bien vouloir vous connecter avec le compte déjà existant. - -Je reste à votre disposition pour tout complément d'information. - -Excellente journée, -L’équipe MonComptePro." -`; - -exports[`returns Diamond members 1`] = ` -"Bonjour, - -Votre demande pour rejoindre l'organisation « 🦄 » a été prise en compte sur https://app.moncomptepro.beta.gouv.fr. - -Vous possédez déjà un compte MonComptePro : - -- pink.diamond@unicorn.xyz -- red.diamond@unicorn.xyz - -Merci de bien vouloir vous connecter avec le compte déjà existant. - -Je reste à votre disposition pour tout complément d'information. - -Excellente journée, -L’équipe MonComptePro." -`; diff --git a/packages/~/moderations/api/src/:id/responses/already_signed.test.tsx b/packages/~/moderations/api/src/:id/responses/already_signed.test.tsx deleted file mode 100644 index 95cefa80..00000000 --- a/packages/~/moderations/api/src/:id/responses/already_signed.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -// - -import { set_moncomptepro_pg } from "@~/app.middleware/set_moncomptepro_pg"; -import { - create_adora_pony_user, - create_pink_diamond_user, - create_red_diamond_user, - create_unicorn_organization, -} from "@~/moncomptepro.database/seed/unicorn"; -import { - add_user_to_organization, - empty_database, - migrate, - pg, -} from "@~/moncomptepro.database/testing"; -import { beforeAll, beforeEach, expect, test } from "bun:test"; -import { Hono } from "hono"; -import { jsxRenderer } from "hono/jsx-renderer"; -import { - type ContextType, - type get_moderation_dto, - type get_organization_member_dto, -} from "../context"; -import already_signed from "./already_signed"; - -// - -beforeAll(migrate); -beforeEach(empty_database); - -test("returns all members", async () => { - const unicorn_organization_id = await given_unicorn_organization(); - - const app = new Hono() - .use("*", jsxRenderer()) - .use("*", set_moncomptepro_pg(pg)) - .get( - "/already_signed", - ({ set }, next) => { - set("domain", "unicorn.xyz"); - set("moderation", { - organization: { cached_libelle: "🦄", id: unicorn_organization_id }, - user: { family_name: "🧟" }, - } as get_moderation_dto); - set("organization_member", {} as get_organization_member_dto); - return next(); - }, - ({ render }) => { - return render(); - }, - ); - - const res = await app.fetch( - new Request("http://localhost:3000/already_signed"), - ); - expect(res.status).toBe(200); - expect(await res.text()).toMatchSnapshot(); -}); - -test("returns Diamond members", async () => { - const unicorn_organization_id = await given_unicorn_organization(); - - const app = new Hono() - .use("*", jsxRenderer()) - .use("*", set_moncomptepro_pg(pg)) - .get( - "/already_signed", - ({ set }, next) => { - set("domain", "unicorn.xyz"); - set("moderation", { - organization: { cached_libelle: "🦄", id: unicorn_organization_id }, - user: { family_name: "Diamond" }, - } as get_moderation_dto); - set("organization_member", {} as get_organization_member_dto); - return next(); - }, - ({ render }) => { - return render(); - }, - ); - - const res = await app.fetch( - new Request("http://localhost:3000/already_signed"), - ); - expect(res.status).toBe(200); - expect(await res.text()).toMatchSnapshot(); -}); - -async function given_unicorn_organization() { - const unicorn_organization_id = await create_unicorn_organization(pg); - const adora_pony_user_id = await create_adora_pony_user(pg); - await add_user_to_organization({ - organization_id: unicorn_organization_id, - user_id: adora_pony_user_id, - }); - const pink_diamond_user_id = await create_pink_diamond_user(pg); - await add_user_to_organization({ - organization_id: unicorn_organization_id, - user_id: pink_diamond_user_id, - }); - const red_diamond_user_id = await create_red_diamond_user(pg); - await add_user_to_organization({ - organization_id: unicorn_organization_id, - user_id: red_diamond_user_id, - }); - return unicorn_organization_id; -} - -async function AlreadySigned() { - return <>{await already_signed()}; -} diff --git a/packages/~/moderations/lib/src/context/rejected.ts b/packages/~/moderations/lib/src/context/rejected.ts index f9424cc8..990010ef 100644 --- a/packages/~/moderations/lib/src/context/rejected.ts +++ b/packages/~/moderations/lib/src/context/rejected.ts @@ -4,16 +4,7 @@ import type { AgentConnect_UserInfo } from "@~/app.middleware/session"; import type { Config } from "@~/crisp.lib/types"; import type { get_moderation_dto } from "@~/moderations.repository/get_moderation"; import type { MonComptePro_PgDatabase } from "@~/moncomptepro.database"; -import { z } from "zod"; - -// - -export const RejectedMessage_Schema = z.object({ - message: z.string().trim(), - subject: z.string().trim(), -}); - -export type RejectedMessage = z.TypeOf; +import type { RejectedMessage } from "../schema/rejected.form"; // diff --git a/packages/~/moderations/lib/src/entities/Moderation.ts b/packages/~/moderations/lib/src/entities/Moderation.ts new file mode 100644 index 00000000..4002c977 --- /dev/null +++ b/packages/~/moderations/lib/src/entities/Moderation.ts @@ -0,0 +1,7 @@ +// + +import type { schema } from "@~/moncomptepro.database"; + +// + +export type Moderation = typeof schema.moderations.$inferSelect; diff --git a/packages/~/moderations/lib/src/schema/rejected.form.ts b/packages/~/moderations/lib/src/schema/rejected.form.ts new file mode 100644 index 00000000..ebece083 --- /dev/null +++ b/packages/~/moderations/lib/src/schema/rejected.form.ts @@ -0,0 +1,12 @@ +// + +import { z } from "zod"; + +// + +export const reject_form_schema = z.object({ + message: z.string().trim(), + subject: z.string().trim(), +}); + +export type RejectedMessage = z.infer; diff --git a/packages/~/moderations/lib/src/schema/validate.form.ts b/packages/~/moderations/lib/src/schema/validate.form.ts new file mode 100644 index 00000000..bf381c88 --- /dev/null +++ b/packages/~/moderations/lib/src/schema/validate.form.ts @@ -0,0 +1,12 @@ +// + +import { z_coerce_boolean } from "@~/app.core/schema/z_coerce_boolean"; +import { z } from "zod"; + +// + +export const validate_form_schema = z.object({ + add_domain: z.string().default("false").pipe(z_coerce_boolean), + add_member: z.enum(["AS_INTERNAL", "AS_EXTERNAL"]), + send_notitfication: z.string().default("false").pipe(z_coerce_boolean), +}); diff --git a/packages/~/moderations/lib/src/usecase/IsUserExternalMember.ts b/packages/~/moderations/lib/src/usecase/IsUserExternalMember.ts new file mode 100644 index 00000000..c081ad1c --- /dev/null +++ b/packages/~/moderations/lib/src/usecase/IsUserExternalMember.ts @@ -0,0 +1,35 @@ +// + +import { + schema, + type MonCompteProDatabaseCradle, +} from "@~/moncomptepro.database"; +import { and, eq } from "drizzle-orm"; + +// + +type UserOrganizationIdPair = { user_id: number; organization_id: number }; + +export function IsUserExternalMember({ pg }: MonCompteProDatabaseCradle) { + return async function is_user_external_member({ + organization_id, + user_id, + }: UserOrganizationIdPair) { + const user_organization = await pg.query.users_organizations.findFirst({ + columns: { is_external: true }, + where: and( + eq(schema.users_organizations.user_id, user_id), + eq(schema.users_organizations.organization_id, organization_id), + ), + }); + + return user_organization?.is_external ?? true; + }; +} + +export type IsUserExternalMemberHandler = ReturnType< + typeof IsUserExternalMember +>; +export type IsUserExternalMemberOutput = Awaited< + ReturnType +>; diff --git a/packages/~/moderations/lib/src/usecase/send_rejected_message_to_user.ts b/packages/~/moderations/lib/src/usecase/send_rejected_message_to_user.ts index f8f606d9..5ae64a62 100644 --- a/packages/~/moderations/lib/src/usecase/send_rejected_message_to_user.ts +++ b/packages/~/moderations/lib/src/usecase/send_rejected_message_to_user.ts @@ -2,10 +2,8 @@ import { NotFoundError } from "@~/app.core/error"; import { z_username } from "@~/app.core/schema/z_username"; import { to as await_to } from "await-to-js"; import consola from "consola"; -import type { - RejectedMessage, - RejectedModeration_Context, -} from "../context/rejected"; +import type { RejectedModeration_Context } from "../context/rejected"; +import type { RejectedMessage } from "../schema/rejected.form"; import { create_and_send_email_to_user } from "./create_and_send_email_to_user"; import { respond_to_ticket } from "./respond_to_ticket"; diff --git a/packages/~/moderations/ui/src/Actions/Actions.tsx b/packages/~/moderations/ui/src/Actions/Actions.tsx new file mode 100644 index 00000000..65fd25eb --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/Actions.tsx @@ -0,0 +1,69 @@ +// + +import { hyper_ref } from "@~/app.core/html"; +import { z_email_domain } from "@~/app.core/schema/z_email_domain"; +import { button } from "@~/app.ui/button"; +import { hx_urls } from "@~/app.urls"; +import { MessageInfo } from "@~/moderations.ui/MessageInfo"; +import { context, type Values } from "./context"; +import { Desicison } from "./Desicison"; +import { MemberInvalid } from "./MemberInvalid"; +import { MemberValid } from "./MemberValid"; + +// + +type ActionProps = { + value: Omit; +}; + +export async function Actions({ value }: ActionProps) { + const { moderation } = value; + + const hx_moderation_reprocess_props = await hx_urls.moderations[ + ":id" + ].$procedures.reprocess.$patch({ + param: { id: moderation.id.toString() }, + }); + + const domain = z_email_domain.parse(moderation.user.email, { + path: ["user.email"], + }); + + return ( + +
+

+ Actions de modération{" "} + beta{" "} +

+ + + +
+ {moderation.moderated_at ? ( + + ) : ( + <> + + + + + )} +
+
+ ); +} diff --git a/packages/~/moderations/ui/src/Actions/AddAsMemberExternal.tsx b/packages/~/moderations/ui/src/Actions/AddAsMemberExternal.tsx new file mode 100644 index 00000000..c1a944ad --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/AddAsMemberExternal.tsx @@ -0,0 +1,34 @@ +// + +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; +import { useContext } from "hono/jsx"; +import { context, valid_context } from "./context"; + +// + +export function AddAsMemberExternal() { + const { $add_as_external_member, is_already_external_member } = + useContext(valid_context); + const { + moderation: { + user: { given_name }, + }, + } = useContext(context); + + return ( +
+ + +
+ ); +} diff --git a/packages/~/moderations/ui/src/Actions/AddAsMemberInternal.tsx b/packages/~/moderations/ui/src/Actions/AddAsMemberInternal.tsx new file mode 100644 index 00000000..9d634edb --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/AddAsMemberInternal.tsx @@ -0,0 +1,30 @@ +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; +import { useContext } from "hono/jsx"; +import { context, valid_context } from "./context"; + +export function AddAsMemberInternal() { + const { $add_as_internal_member, is_already_internal_member } = + useContext(valid_context); + const { + moderation: { + user: { given_name }, + }, + } = useContext(context); + + return ( +
+ + +
+ ); +} diff --git a/packages/~/moderations/ui/src/Actions/AddDomain.tsx b/packages/~/moderations/ui/src/Actions/AddDomain.tsx new file mode 100644 index 00000000..76fb0824 --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/AddDomain.tsx @@ -0,0 +1,23 @@ +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; +import { useContext } from "hono/jsx"; +import { context, valid_context } from "./context"; + +export function AddDomain() { + const { $add_domain } = useContext(valid_context); + const { domain } = useContext(context); + + return ( +
+ + +
+ ); +} diff --git a/packages/~/moderations/api/src/:id/Desicison.tsx b/packages/~/moderations/ui/src/Actions/Desicison.tsx similarity index 88% rename from packages/~/moderations/api/src/:id/Desicison.tsx rename to packages/~/moderations/ui/src/Actions/Desicison.tsx index bd9be191..5ca77230 100644 --- a/packages/~/moderations/api/src/:id/Desicison.tsx +++ b/packages/~/moderations/ui/src/Actions/Desicison.tsx @@ -2,11 +2,11 @@ import { fieldset } from "@~/app.ui/form"; import { useContext } from "hono/jsx"; -import { Desicison_Context } from "./Desicison_Context"; +import { context } from "./context"; // export function Desicison() { - const { $accept, $decision_form, $reject } = useContext(Desicison_Context); + const { $accept, $decision_form, $reject } = useContext(context); const { base, element, legend } = fieldset(); return ( diff --git a/packages/~/moderations/api/src/:id/Member_Invalid.tsx b/packages/~/moderations/ui/src/Actions/MemberInvalid.tsx similarity index 62% rename from packages/~/moderations/api/src/:id/Member_Invalid.tsx rename to packages/~/moderations/ui/src/Actions/MemberInvalid.tsx index ef2ef67b..13c0fd3c 100644 --- a/packages/~/moderations/api/src/:id/Member_Invalid.tsx +++ b/packages/~/moderations/ui/src/Actions/MemberInvalid.tsx @@ -4,64 +4,24 @@ import { Htmx_Events, hx_disabled_form_elements } from "@~/app.core/htmx"; import { button } from "@~/app.ui/button"; import { copy_value_to_clipboard } from "@~/app.ui/button/scripts"; import { fieldset } from "@~/app.ui/form"; -import { hx_urls, urls } from "@~/app.urls"; -import type { InferRequestType } from "hono"; +import { hx_urls } from "@~/app.urls"; +import { reject_form_schema } from "@~/moderations.lib/schema/rejected.form"; import { useContext } from "hono/jsx"; -import { usePageRequestContext } from "./context"; -import { Desicison_Context } from "./Desicison_Context"; -import * as accountant from "./responses/accountant"; -import * as already_signed from "./responses/already_signed"; -import * as chorus_pro_error from "./responses/chorus_pro_error"; -import * as contractors from "./responses/contractors"; -import * as first_last_name from "./responses/first_last_name"; -import * as invalid_job from "./responses/invalid_job"; -import * as invalid_name_job from "./responses/invalid_name_job"; -import * as link_with_eduction_gouv_fr from "./responses/link_with_eduction_gouv_fr"; -import * as link_with_organization from "./responses/link_with_organization"; -import * as mobilic from "./responses/mobilic"; -import * as use_official_email from "./responses/use_official_email"; -import * as use_pro_email from "./responses/use_pro_email"; +import { ResponseMessageSelector } from "./ResponseMessageSelector"; +import { context, reject_context } from "./context"; // -const reponse_templates = [ - first_last_name, - link_with_organization, - use_pro_email, - use_official_email, - already_signed, - link_with_eduction_gouv_fr, - mobilic, - contractors, - accountant, - invalid_name_job, - chorus_pro_error, - invalid_job, -]; - -// - -export async function Member_Invalid() { - const { - var: { moderation }, - } = usePageRequestContext(); - - const { - $destination, - $reject, - $message, - $object, - $decision_form: $form, - } = useContext(Desicison_Context); +export async function MemberInvalid() { + const { moderation, $reject, $decision_form } = useContext(context); + const { $destination, $message, $object } = useContext(reject_context); const { base, element } = fieldset(); - const $patch = urls.moderations[":id"].$procedures.rejected.$patch; - type FormNames = keyof InferRequestType["form"]; return (

@@ -134,7 +94,7 @@ export async function Member_Invalid() { class="fr-input" type="text" id={$object} - name={"subject" as FormNames} + name={reject_form_schema.keyof().Enum.subject} value={`[MonComptePro] Demande pour rejoindre « ${moderation.organization.cached_libelle} »`} /> @@ -182,26 +142,3 @@ export async function Member_Invalid() { ); } - -function ResponseMessageSelector() { - const { $message, $select } = useContext(Desicison_Context); - return ( - - ); -} diff --git a/packages/~/moderations/ui/src/Actions/MemberValid.tsx b/packages/~/moderations/ui/src/Actions/MemberValid.tsx new file mode 100644 index 00000000..fa436963 --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/MemberValid.tsx @@ -0,0 +1,75 @@ +// + +import { Htmx_Events, hx_include } from "@~/app.core/htmx"; +import { button } from "@~/app.ui/button"; +import { fieldset } from "@~/app.ui/form"; +import { hx_urls } from "@~/app.urls"; +import { useContext } from "hono/jsx"; +import { AddAsMemberExternal } from "./AddAsMemberExternal"; +import { AddAsMemberInternal } from "./AddAsMemberInternal"; +import { AddDomain } from "./AddDomain"; +import { context, valid_context } from "./context"; +import { SendNotification } from "./SendNotification"; + +// + +export async function MemberValid() { + const { moderation, $decision_form, $accept, query_is_user_external_member } = + useContext(context); + const context_value = useContext(valid_context); + const { $add_domain } = context_value; + const { base, element } = fieldset(); + const hx_path_validate_moderation = await hx_urls.moderations[ + ":id" + ].$procedures.validate.$patch({ + param: { id: moderation.id.toString() }, + }); + + const is_already_external_member = await query_is_user_external_member({ + organization_id: moderation.organization.id, + user_id: moderation.user.id, + }); + + return ( + + + + ); +} diff --git a/packages/~/moderations/ui/src/Actions/ResponseMessageSelector.tsx b/packages/~/moderations/ui/src/Actions/ResponseMessageSelector.tsx new file mode 100644 index 00000000..91f58df6 --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/ResponseMessageSelector.tsx @@ -0,0 +1,30 @@ +// + +import { useContext } from "hono/jsx"; +import { reject_context } from "./context"; +import { reponse_templates } from "./responses"; + +// + +export function ResponseMessageSelector() { + const { $message, $select } = useContext(reject_context); + return ( + + ); +} diff --git a/packages/~/moderations/ui/src/Actions/SendNotification.tsx b/packages/~/moderations/ui/src/Actions/SendNotification.tsx new file mode 100644 index 00000000..e5a8721e --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/SendNotification.tsx @@ -0,0 +1,37 @@ +// + +import { Moderation_Type_Schema } from "@~/moderations.lib/Moderation_Type"; +import { validate_form_schema } from "@~/moderations.lib/schema/validate.form"; +import { useContext } from "hono/jsx"; +import { context, valid_context } from "./context"; + +// + +export function SendNotification() { + const { + moderation: { + type, + user: { email }, + }, + } = useContext(context); + const { $send_notification } = useContext(valid_context); + + return ( +
+ + +
+ ); +} diff --git a/packages/~/moderations/ui/src/Actions/context.ts b/packages/~/moderations/ui/src/Actions/context.ts new file mode 100644 index 00000000..6bc6b3df --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/context.ts @@ -0,0 +1,42 @@ +// + +import { hyper_ref } from "@~/app.core/html"; +import type { SimplifyDeep } from "@~/app.core/types"; +import type { Moderation } from "@~/moderations.lib/entities/Moderation"; +import type { IsUserExternalMemberHandler } from "@~/moderations.lib/usecase/IsUserExternalMember"; +import type { Organization } from "@~/organizations.lib/entities/Organization"; +import type { User } from "@~/users.lib/entities/User"; +import type { SuggestSameUserEmailsHandler } from "@~/users.lib/usecase/SuggestSameUserEmails"; +import { createContext } from "hono/jsx"; + +export interface Values { + domain: string; + moderation: SimplifyDeep< + Pick & { + organization: Pick; + user: Pick; + } + >; + $reject: string; + $accept: string; + $decision_form: string; + query_suggest_same_user_emails: SuggestSameUserEmailsHandler; + query_is_user_external_member: IsUserExternalMemberHandler; +} +export const context = createContext(null as any); + +// +export const reject_context = createContext({ + $destination: hyper_ref(), + $message: hyper_ref(), + $object: hyper_ref(), + $select: hyper_ref(), +}); +export const valid_context = createContext({ + $add_as_external_member: hyper_ref(), + $add_as_internal_member: hyper_ref(), + $add_domain: hyper_ref(), + $send_notification: hyper_ref(), + is_already_internal_member: false, + is_already_external_member: false, +}); diff --git a/packages/~/moderations/ui/src/Actions/index.ts b/packages/~/moderations/ui/src/Actions/index.ts new file mode 100644 index 00000000..a2287350 --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./Actions"; diff --git a/packages/~/moderations/ui/src/Actions/responses/__snapshots__/already_signed.test.tsx.snap b/packages/~/moderations/ui/src/Actions/responses/__snapshots__/already_signed.test.tsx.snap new file mode 100644 index 00000000..bfbb9126 --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/responses/__snapshots__/already_signed.test.tsx.snap @@ -0,0 +1,21 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`returns all members 1`] = ` +"Bonjour, + +Votre demande pour rejoindre l'organisation « 🦄 » a été prise en compte sur https://app.moncomptepro.beta.gouv.fr. + +Vous possédez déjà un compte MonComptePro : + +- 🦄@unicorn.xyz +- 🐷@unicorn.xyz +- 🧧@unicorn.xyz + +Merci de bien vouloir vous connecter avec le compte déjà existant. + +Je reste à votre disposition pour tout complément d'information. + +Excellente journée, +L’équipe MonComptePro. +" +`; diff --git a/packages/~/moderations/api/src/:id/responses/accountant.tsx b/packages/~/moderations/ui/src/Actions/responses/accountant.tsx similarity index 76% rename from packages/~/moderations/api/src/:id/responses/accountant.tsx rename to packages/~/moderations/ui/src/Actions/responses/accountant.tsx index 77dbd327..172550e5 100644 --- a/packages/~/moderations/api/src/:id/responses/accountant.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/accountant.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "refus prestataires"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/ui/src/Actions/responses/already_signed.test.tsx b/packages/~/moderations/ui/src/Actions/responses/already_signed.test.tsx new file mode 100644 index 00000000..8dc4538d --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/responses/already_signed.test.tsx @@ -0,0 +1,124 @@ +// + +import { render_md } from "@~/app.ui/testing"; +import { expect, test } from "bun:test"; +import { context } from "../context"; +import already_signed from "./already_signed"; + +// + +test("returns all members", async () => { + expect( + await render_md( + [ + "🦄@unicorn.xyz", + "🐷@unicorn.xyz", + "🧧@unicorn.xyz", + ], + query_is_user_external_member: async () => false, + }} + > + + , + ), + ).toMatchSnapshot(); +}); + +function AlreadySigned() { + return <>{already_signed()}; +} + +// import { set_moncomptepro_pg } from "@~/app.middleware/set_moncomptepro_pg"; +// import { +// create_adora_pony_user, +// create_pink_diamond_user, +// create_red_diamond_user, +// create_unicorn_organization, +// } from "@~/moncomptepro.database/seed/unicorn"; +// import { +// add_user_to_organization, +// empty_database, +// migrate, +// pg, +// } from "@~/moncomptepro.database/testing"; +// import { beforeAll, beforeEach, expect, test } from "bun:test"; +// import { Hono } from "hono"; +// import { jsxRenderer } from "hono/jsx-renderer"; +// // import { +// // type ContextType, +// // type get_moderation_dto, +// // type get_organization_member_dto, +// // } from "../context"; +// import already_signed from "./already_signed"; + +// // + +// // beforeAll(migrate); +// // beforeEach(empty_database); + + +// // test("returns Diamond members", async () => { +// // const unicorn_organization_id = await given_unicorn_organization(); + +// // const app = new Hono() +// // .use("*", jsxRenderer()) +// // .use("*", set_moncomptepro_pg(pg)) +// // .get( +// // "/already_signed", +// // ({ set }, next) => { +// // set("domain", "unicorn.xyz"); +// // set("moderation", { +// // organization: { cached_libelle: "🦄", id: unicorn_organization_id }, +// // user: { family_name: "Diamond" }, +// // } as get_moderation_dto); +// // set("organization_member", {} as get_organization_member_dto); +// // return next(); +// // }, +// // ({ render }) => { +// // return render(); +// // }, +// // ); + +// // const res = await app.fetch( +// // new Request("http://localhost:3000/already_signed"), +// // ); +// // expect(res.status).toBe(200); +// // expect(await res.text()).toMatchSnapshot(); +// // }); + +// // async function given_unicorn_organization() { +// // const unicorn_organization_id = await create_unicorn_organization(pg); +// // const adora_pony_user_id = await create_adora_pony_user(pg); +// // await add_user_to_organization({ +// // organization_id: unicorn_organization_id, +// // user_id: adora_pony_user_id, +// // }); +// // const pink_diamond_user_id = await create_pink_diamond_user(pg); +// // await add_user_to_organization({ +// // organization_id: unicorn_organization_id, +// // user_id: pink_diamond_user_id, +// // }); +// // const red_diamond_user_id = await create_red_diamond_user(pg); +// // await add_user_to_organization({ +// // organization_id: unicorn_organization_id, +// // user_id: red_diamond_user_id, +// // }); +// // return unicorn_organization_id; +// // } + +// // async function AlreadySigned() { +// // return <>{await already_signed()}; +// // } diff --git a/packages/~/moderations/api/src/:id/responses/already_signed.tsx b/packages/~/moderations/ui/src/Actions/responses/already_signed.tsx similarity index 55% rename from packages/~/moderations/api/src/:id/responses/already_signed.tsx rename to packages/~/moderations/ui/src/Actions/responses/already_signed.tsx index 6c3ced2d..cecaa2ef 100644 --- a/packages/~/moderations/api/src/:id/responses/already_signed.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/already_signed.tsx @@ -1,25 +1,17 @@ // -import type { MonComptePro_Pg_Context } from "@~/app.middleware/moncomptepro_pg"; -import { get_emails_by_organization_id } from "@~/users.repository/get_emails_by_organization_id"; -import { useRequestContext } from "hono/jsx-renderer"; +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Vous possédez déjà un compte MonComptePro"; export default async function template() { - const { - var: { moderation }, - } = usePageRequestContext(); + const { moderation, query_suggest_same_user_emails } = useContext(context); - const { - var: { moncomptepro_pg }, - } = useRequestContext(); - - const members_email = await get_emails_by_organization_id(moncomptepro_pg, { - organization_id: moderation.organization.id, + const members_email = await query_suggest_same_user_emails({ family_name: moderation.user.family_name ?? "", + organization_id: moderation.organization.id, }); return dedent` @@ -29,7 +21,7 @@ export default async function template() { Vous possédez déjà un compte MonComptePro : - - ${members_email.map(({ email }) => email).join("\n- ")} + - ${members_email.join("\n- ")} Merci de bien vouloir vous connecter avec le compte déjà existant. diff --git a/packages/~/moderations/api/src/:id/responses/chorus_pro_error.tsx b/packages/~/moderations/ui/src/Actions/responses/chorus_pro_error.tsx similarity index 82% rename from packages/~/moderations/api/src/:id/responses/chorus_pro_error.tsx rename to packages/~/moderations/ui/src/Actions/responses/chorus_pro_error.tsx index 337695a7..a2a64fcc 100644 --- a/packages/~/moderations/api/src/:id/responses/chorus_pro_error.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/chorus_pro_error.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Erreur Chorus Pro"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/contractors.tsx b/packages/~/moderations/ui/src/Actions/responses/contractors.tsx similarity index 79% rename from packages/~/moderations/api/src/:id/responses/contractors.tsx rename to packages/~/moderations/ui/src/Actions/responses/contractors.tsx index 9b26528c..e4fc98fd 100644 --- a/packages/~/moderations/api/src/:id/responses/contractors.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/contractors.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "refus comptable"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/first_last_name.tsx b/packages/~/moderations/ui/src/Actions/responses/first_last_name.tsx similarity index 82% rename from packages/~/moderations/api/src/:id/responses/first_last_name.tsx rename to packages/~/moderations/ui/src/Actions/responses/first_last_name.tsx index 71a6a073..dd77400f 100644 --- a/packages/~/moderations/api/src/:id/responses/first_last_name.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/first_last_name.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "nom et prénom et fonction"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name, siret }, - }, + moderation: { + organization: { cached_libelle: organization_name, siret }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/ui/src/Actions/responses/index.ts b/packages/~/moderations/ui/src/Actions/responses/index.ts new file mode 100644 index 00000000..99f444bc --- /dev/null +++ b/packages/~/moderations/ui/src/Actions/responses/index.ts @@ -0,0 +1,31 @@ +// + +import * as accountant from "./accountant"; +import * as already_signed from "./already_signed"; +import * as chorus_pro_error from "./chorus_pro_error"; +import * as contractors from "./contractors"; +import * as first_last_name from "./first_last_name"; +import * as invalid_job from "./invalid_job"; +import * as invalid_name_job from "./invalid_name_job"; +import * as link_with_eduction_gouv_fr from "./link_with_eduction_gouv_fr"; +import * as link_with_organization from "./link_with_organization"; +import * as mobilic from "./mobilic"; +import * as use_official_email from "./use_official_email"; +import * as use_pro_email from "./use_pro_email"; + +// + +export const reponse_templates = [ + first_last_name, + link_with_organization, + use_pro_email, + use_official_email, + already_signed, + link_with_eduction_gouv_fr, + mobilic, + contractors, + accountant, + invalid_name_job, + chorus_pro_error, + invalid_job, +]; diff --git a/packages/~/moderations/api/src/:id/responses/invalid_job.tsx b/packages/~/moderations/ui/src/Actions/responses/invalid_job.tsx similarity index 79% rename from packages/~/moderations/api/src/:id/responses/invalid_job.tsx rename to packages/~/moderations/ui/src/Actions/responses/invalid_job.tsx index 079e55e0..7479947d 100644 --- a/packages/~/moderations/api/src/:id/responses/invalid_job.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/invalid_job.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "modification fonction"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/invalid_name_job.tsx b/packages/~/moderations/ui/src/Actions/responses/invalid_name_job.tsx similarity index 100% rename from packages/~/moderations/api/src/:id/responses/invalid_name_job.tsx rename to packages/~/moderations/ui/src/Actions/responses/invalid_name_job.tsx diff --git a/packages/~/moderations/api/src/:id/responses/link_with_eduction_gouv_fr.tsx b/packages/~/moderations/ui/src/Actions/responses/link_with_eduction_gouv_fr.tsx similarity index 83% rename from packages/~/moderations/api/src/:id/responses/link_with_eduction_gouv_fr.tsx rename to packages/~/moderations/ui/src/Actions/responses/link_with_eduction_gouv_fr.tsx index a1caab0a..46f6660c 100644 --- a/packages/~/moderations/api/src/:id/responses/link_with_eduction_gouv_fr.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/link_with_eduction_gouv_fr.tsx @@ -1,19 +1,18 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Pas de légitimité - Ministère de l'Éducation"; export default function template() { const { - var: { - domain, - moderation: { - organization: { cached_libelle: organization_name }, - }, + domain, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/link_with_organization.tsx b/packages/~/moderations/ui/src/Actions/responses/link_with_organization.tsx similarity index 77% rename from packages/~/moderations/api/src/:id/responses/link_with_organization.tsx rename to packages/~/moderations/ui/src/Actions/responses/link_with_organization.tsx index 00f3c910..ba5accf2 100644 --- a/packages/~/moderations/api/src/:id/responses/link_with_organization.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/link_with_organization.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Quel lien avec l'organisation ?"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/mobilic.tsx b/packages/~/moderations/ui/src/Actions/responses/mobilic.tsx similarity index 76% rename from packages/~/moderations/api/src/:id/responses/mobilic.tsx rename to packages/~/moderations/ui/src/Actions/responses/mobilic.tsx index 2f3fd844..eb55daff 100644 --- a/packages/~/moderations/api/src/:id/responses/mobilic.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/mobilic.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "livreurs / mobilic"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/use_official_email.tsx b/packages/~/moderations/ui/src/Actions/responses/use_official_email.tsx similarity index 77% rename from packages/~/moderations/api/src/:id/responses/use_official_email.tsx rename to packages/~/moderations/ui/src/Actions/responses/use_official_email.tsx index 8b79b82b..6171c302 100644 --- a/packages/~/moderations/api/src/:id/responses/use_official_email.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/use_official_email.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Merci d'utiliser votre adresse officielle de contact"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/responses/use_pro_email.tsx b/packages/~/moderations/ui/src/Actions/responses/use_pro_email.tsx similarity index 76% rename from packages/~/moderations/api/src/:id/responses/use_pro_email.tsx rename to packages/~/moderations/ui/src/Actions/responses/use_pro_email.tsx index 717cda1a..8cd31a8f 100644 --- a/packages/~/moderations/api/src/:id/responses/use_pro_email.tsx +++ b/packages/~/moderations/ui/src/Actions/responses/use_pro_email.tsx @@ -1,18 +1,17 @@ // +import { useContext } from "hono/jsx"; import { dedent } from "ts-dedent"; -import { usePageRequestContext } from "../context"; +import { context } from "../context"; export const label = "Merci d'utiliser votre adresse email professionnelle"; export default function template() { const { - var: { - moderation: { - organization: { cached_libelle: organization_name }, - }, + moderation: { + organization: { cached_libelle: organization_name }, }, - } = usePageRequestContext(); + } = useContext(context); return dedent` Bonjour, diff --git a/packages/~/moderations/api/src/:id/Domain_Organization.tsx b/packages/~/moderations/ui/src/DomainsByOrganization/DomainsByOrganization.tsx similarity index 65% rename from packages/~/moderations/api/src/:id/Domain_Organization.tsx rename to packages/~/moderations/ui/src/DomainsByOrganization/DomainsByOrganization.tsx index 39d86e73..984ab369 100644 --- a/packages/~/moderations/api/src/:id/Domain_Organization.tsx +++ b/packages/~/moderations/ui/src/DomainsByOrganization/DomainsByOrganization.tsx @@ -5,20 +5,28 @@ import { hx_trigger_from_body } from "@~/app.core/htmx"; import { Loader } from "@~/app.ui/loader/Loader"; import { formattedPlural } from "@~/app.ui/plurial"; import { hx_urls } from "@~/app.urls"; +import type { Organization } from "@~/organizations.lib/entities/Organization"; import { ORGANISATION_EVENTS } from "@~/organizations.lib/event"; -import { usePageRequestContext } from "./context"; // -export async function Domain_Organization() { +type Props = { + organization: Pick; + query_domain_count: Promise; +}; +export async function DomainsByOrganization(props: Props) { const $describedby = hyper_ref(); - const { - var: { - moderation: { organization }, - query_domain_count, - }, - } = usePageRequestContext(); + const { organization, query_domain_count } = props; const count = await query_domain_count; + const query_domains_by_organization_id = await hx_urls.organizations[ + ":id" + ].domains.$get({ + param: { + id: organization.id.toString(), + }, + query: { describedby: $describedby }, + }); + return (
@@ -33,12 +41,7 @@ export async function Domain_Organization() {
{ expect( - await renderHTML( + await render_html( & { organization: Pick; - type: string; user: Pick; }; }; diff --git a/packages/~/moderations/ui/src/MessageInfo/index.ts b/packages/~/moderations/ui/src/MessageInfo/index.ts index 3cb72020..6c7bac9d 100644 --- a/packages/~/moderations/ui/src/MessageInfo/index.ts +++ b/packages/~/moderations/ui/src/MessageInfo/index.ts @@ -1,3 +1,3 @@ // -export { MessageInfo } from "./MessageInfo"; +export * from "./MessageInfo"; diff --git a/packages/~/moderations/ui/src/OrganizationsByUser/OrganizationsByUser.tsx b/packages/~/moderations/ui/src/OrganizationsByUser/OrganizationsByUser.tsx new file mode 100644 index 00000000..94c40d03 --- /dev/null +++ b/packages/~/moderations/ui/src/OrganizationsByUser/OrganizationsByUser.tsx @@ -0,0 +1,57 @@ +// + +import { hyper_ref } from "@~/app.core/html"; +import { hx_trigger_from_body } from "@~/app.core/htmx"; +import { Loader } from "@~/app.ui/loader/Loader"; +import { formattedPlural } from "@~/app.ui/plurial"; +import { hx_urls } from "@~/app.urls"; +import { ORGANISATION_EVENTS } from "@~/organizations.lib/event"; +import type { User } from "@~/users.lib/entities/User"; +import type { CountUserMembershipsHandler } from "@~/users.lib/usecase/CountUserMemberships"; + +// + +type Props = { + user: Pick; + query_organization_count: CountUserMembershipsHandler; +}; +export async function OrganizationsByUser(props: Props) { + const { user, query_organization_count } = props; + const $describedby = hyper_ref(); + const count = await query_organization_count(user.id); + const hx_get_organizations_by_user = await hx_urls.users[ + ":id" + ].organizations.$get({ + param: { id: user.id.toString() }, + query: { describedby: $describedby }, + }); + + return ( +
+
+ +

+ 🏢 Member de {count}{" "} + {formattedPlural(count, { + one: "organisation", + other: "organisations", + })} +

+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/packages/~/moderations/ui/src/OrganizationsByUser/index.ts b/packages/~/moderations/ui/src/OrganizationsByUser/index.ts new file mode 100644 index 00000000..406970fc --- /dev/null +++ b/packages/~/moderations/ui/src/OrganizationsByUser/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./OrganizationsByUser"; diff --git a/packages/~/moderations/api/src/:id/Members_Of_Organization_Table.tsx b/packages/~/moderations/ui/src/UsersByOrganization/UsersByOrganization.tsx similarity index 79% rename from packages/~/moderations/api/src/:id/Members_Of_Organization_Table.tsx rename to packages/~/moderations/ui/src/UsersByOrganization/UsersByOrganization.tsx index 99728523..2d17aab5 100644 --- a/packages/~/moderations/api/src/:id/Members_Of_Organization_Table.tsx +++ b/packages/~/moderations/ui/src/UsersByOrganization/UsersByOrganization.tsx @@ -5,20 +5,21 @@ import { hx_include, hx_trigger_from_body } from "@~/app.core/htmx"; import { Loader } from "@~/app.ui/loader/Loader"; import { formattedPlural } from "@~/app.ui/plurial"; import { hx_urls } from "@~/app.urls"; +import type { Organization } from "@~/organizations.lib/entities/Organization"; import { ORGANISATION_EVENTS } from "@~/organizations.lib/event"; import { match, P } from "ts-pattern"; -import { usePageRequestContext } from "./context"; // -export async function Members_Of_Organization_Table() { +type Props = { + organization: Pick; + query_members_count: Promise; +}; +export async function UsersByOrganization(props: Props) { const $describedby = hyper_ref(); const $page_ref = hyper_ref(); - const { - var: { moderation, query_organization_members_count }, - } = usePageRequestContext(); - - const count = await query_organization_members_count; + const { organization, query_members_count } = props; + const count = await query_members_count; const isOpen = match(count) .with(0, () => false) .with(P.number.between(1, 3), () => true) @@ -42,7 +43,7 @@ export async function Members_Of_Organization_Table() { class="fr-table" {...await hx_urls.organizations[":id"].members.$get({ param: { - id: moderation.organization_id.toString(), + id: organization.id.toString(), }, query: { describedby: $describedby, page_ref: $page_ref }, })} diff --git a/packages/~/moderations/ui/src/UsersByOrganization/index.ts b/packages/~/moderations/ui/src/UsersByOrganization/index.ts new file mode 100644 index 00000000..27bdcd33 --- /dev/null +++ b/packages/~/moderations/ui/src/UsersByOrganization/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./UsersByOrganization"; diff --git a/packages/~/organizations/repository/src/count_organization_members.test.ts b/packages/~/organizations/repository/src/count_organization_members.test.ts new file mode 100644 index 00000000..65fcb2fa --- /dev/null +++ b/packages/~/organizations/repository/src/count_organization_members.test.ts @@ -0,0 +1,72 @@ +// + +import { create_cactus_organization } from "@~/moncomptepro.database/seed/captus"; +import { + create_adora_pony_user, + create_pink_diamond_user, + create_red_diamond_user, + create_unicorn_organization, +} from "@~/moncomptepro.database/seed/unicorn"; +import { + add_user_to_organization, + empty_database, + migrate, + pg, +} from "@~/moncomptepro.database/testing"; +import { beforeAll, beforeEach, expect, test } from "bun:test"; +import { count_organization_members } from "./count_organization_members"; + +// + +beforeAll(migrate); +beforeEach(empty_database); + +// + +test("returns no member", async () => { + const unicorn_organization_id = await create_unicorn_organization(pg); + + const domain_unicorn = await count_organization_members(pg, { + organization_id: unicorn_organization_id, + }); + + expect(domain_unicorn).toEqual(0); +}); + +test("returns 1 member", async () => { + await create_cactus_organization(pg); + const unicorn_organization_id = await create_unicorn_organization(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: await create_pink_diamond_user(pg), + }); + + const domain_unicorn = await count_organization_members(pg, { + organization_id: unicorn_organization_id, + }); + + expect(domain_unicorn).toEqual(1); +}); + +test("returns 3 member", async () => { + await create_cactus_organization(pg); + const unicorn_organization_id = await create_unicorn_organization(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: await create_pink_diamond_user(pg), + }); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: await create_adora_pony_user(pg), + }); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: await create_red_diamond_user(pg), + }); + + const domain_unicorn = await count_organization_members(pg, { + organization_id: unicorn_organization_id, + }); + + expect(domain_unicorn).toEqual(3); +}); diff --git a/packages/~/organizations/repository/src/count_organization_members.ts b/packages/~/organizations/repository/src/count_organization_members.ts new file mode 100644 index 00000000..8612e7ed --- /dev/null +++ b/packages/~/organizations/repository/src/count_organization_members.ts @@ -0,0 +1,22 @@ +// + +import { schema, type MonComptePro_PgDatabase } from "@~/moncomptepro.database"; +import { count as drizzle_count, eq } from "drizzle-orm"; + +// + +export async function count_organization_members( + pg: MonComptePro_PgDatabase, + { organization_id }: { organization_id: number }, +) { + const [{ value: count }] = await pg + .select({ value: drizzle_count() }) + .from(schema.users_organizations) + .where(eq(schema.users_organizations.organization_id, organization_id)); + + return count; +} + +export type count_organization_members_dto = Awaited< + ReturnType +>; diff --git a/packages/~/organizations/ui/src/info/About.test.tsx b/packages/~/organizations/ui/src/info/About.test.tsx index bdfd6b75..710c2270 100644 --- a/packages/~/organizations/ui/src/info/About.test.tsx +++ b/packages/~/organizations/ui/src/info/About.test.tsx @@ -1,12 +1,12 @@ // -import { renderHTML } from "@~/app.ui/testing"; +import { render_html } from "@~/app.ui/testing"; import { expect, test } from "bun:test"; import { About } from "./About"; test("render about section", async () => { expect( - await renderHTML( + await render_html( - +
diff --git a/packages/~/users/api/src/:id/organizations/context.tsx b/packages/~/users/api/src/:id/organizations/context.tsx index ddc8d111..ab099372 100644 --- a/packages/~/users/api/src/:id/organizations/context.tsx +++ b/packages/~/users/api/src/:id/organizations/context.tsx @@ -1,6 +1,10 @@ // -import type { Entity_Schema, Pagination_Schema } from "@~/app.core/schema"; +import { + DescribedBy_Schema, + Pagination_Schema, + type Entity_Schema, +} from "@~/app.core/schema"; import type { App_Context } from "@~/app.middleware/context"; import type { get_organizations_by_user_id_dto } from "@~/organizations.repository/get_organizations_by_user_id"; import type { Env } from "hono"; @@ -9,6 +13,10 @@ import type { z } from "zod"; // +export const query_schema = DescribedBy_Schema.merge(Pagination_Schema); + +// + export interface ContextVariablesType extends Env { Variables: { organizations: Awaited["organizations"]; @@ -22,7 +30,7 @@ export type ContextType = App_Context & ContextVariablesType; type PageInputType = { out: { param: z.input; - query: z.input; + query: z.input; }; }; diff --git a/packages/~/users/api/src/:id/organizations/index.tsx b/packages/~/users/api/src/:id/organizations/index.tsx index 6308981e..8b077116 100644 --- a/packages/~/users/api/src/:id/organizations/index.tsx +++ b/packages/~/users/api/src/:id/organizations/index.tsx @@ -1,12 +1,12 @@ // import { zValidator } from "@hono/zod-validator"; -import { Entity_Schema, Pagination_Schema } from "@~/app.core/schema"; +import { DescribedBy_Schema, Entity_Schema, Pagination_Schema } from "@~/app.core/schema"; import { get_organizations_by_user_id } from "@~/organizations.repository/get_organizations_by_user_id"; import { Hono } from "hono"; import { jsxRenderer } from "hono/jsx-renderer"; import { EmptyTable, Table } from "./Table"; -import type { ContextType } from "./context"; +import { query_schema, type ContextType } from "./context"; // @@ -14,7 +14,7 @@ export default new Hono().get( "/", jsxRenderer(), zValidator("param", Entity_Schema), - zValidator("query", Pagination_Schema), + zValidator("query", query_schema), async function set_moderation({ req, set, var: { moncomptepro_pg } }, next) { const { id: user_id } = req.valid("param"); const pagination = req.valid("query"); diff --git a/packages/~/users/lib/src/usecase/CountUserMemberships.test.ts b/packages/~/users/lib/src/usecase/CountUserMemberships.test.ts new file mode 100644 index 00000000..45508666 --- /dev/null +++ b/packages/~/users/lib/src/usecase/CountUserMemberships.test.ts @@ -0,0 +1,51 @@ +// + +import { create_cactus_organization } from "@~/moncomptepro.database/seed/captus"; +import { + create_pink_diamond_user, + create_unicorn_organization, +} from "@~/moncomptepro.database/seed/unicorn"; +import { + add_user_to_organization, + empty_database, + migrate, + pg, +} from "@~/moncomptepro.database/testing"; +import { beforeAll, beforeEach, expect, test } from "bun:test"; +import { CountUserMemberships } from "./CountUserMemberships"; + +// + +beforeAll(migrate); +beforeEach(empty_database); + +const count_user_merbership = CountUserMemberships({ pg }); + +// + +test("returns no membership", async () => { + const pink_diamond_user_id = await create_pink_diamond_user(pg); + + const memberships = await count_user_merbership(pink_diamond_user_id); + + expect(memberships).toBe(0); +}); + +test("returns two memberships", async () => { + const pink_diamond_user_id = await create_pink_diamond_user(pg); + + const unicorn_organization_id = await create_unicorn_organization(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: pink_diamond_user_id, + }); + const cactus_organization_id = await create_cactus_organization(pg); + await add_user_to_organization({ + organization_id: cactus_organization_id, + user_id: pink_diamond_user_id, + }); + + const memberships = await count_user_merbership(pink_diamond_user_id); + + expect(memberships).toBe(2); +}); diff --git a/packages/~/users/lib/src/usecase/CountUserMemberships.ts b/packages/~/users/lib/src/usecase/CountUserMemberships.ts new file mode 100644 index 00000000..6e70d821 --- /dev/null +++ b/packages/~/users/lib/src/usecase/CountUserMemberships.ts @@ -0,0 +1,28 @@ +// + +import { + schema, + type MonCompteProDatabaseCradle, +} from "@~/moncomptepro.database"; +import { count as drizzle_count, eq } from "drizzle-orm"; + +// + +export function CountUserMemberships({ pg }: MonCompteProDatabaseCradle) { + return async function count_user_merbership(user_id: number) { + const [{ value: count }] = await pg + .select({ value: drizzle_count() }) + .from(schema.users_organizations) + .where(eq(schema.users_organizations.user_id, user_id)); + + return count; + }; +} + +export type CountUserMembershipsHandler = ReturnType< + typeof CountUserMemberships +>; + +export type count_user_merbership_dto = Awaited< + ReturnType +>; diff --git a/packages/~/users/lib/src/usecase/GetUserInfo.ts b/packages/~/users/lib/src/usecase/GetUserInfo.ts new file mode 100644 index 00000000..0f8aa97b --- /dev/null +++ b/packages/~/users/lib/src/usecase/GetUserInfo.ts @@ -0,0 +1,31 @@ +// + +import { NotFoundError } from "@~/app.core/error"; +import type { MonCompteProDatabaseCradle } from "@~/moncomptepro.database"; + +// + +export function GetUserInfo({ pg }: MonCompteProDatabaseCradle) { + return async function get_user_info(id: number) { + const organization = await pg.query.users.findFirst({ + columns: { + id: true, + email: true, + given_name: true, + family_name: true, + phone_number: true, + job: true, + last_sign_in_at: true, + created_at: true, + sign_in_count: true, + }, + where: (table, { eq }) => eq(table.id, id), + }); + + if (!organization) throw new NotFoundError(`User ${id} not found.`); + return organization; + }; +} + +export type GetUserInfoHandler = ReturnType; +export type GetUserInfoOutput = Awaited>; diff --git a/packages/~/users/lib/src/usecase/SuggestSameUserEmails.test.ts b/packages/~/users/lib/src/usecase/SuggestSameUserEmails.test.ts new file mode 100644 index 00000000..6dff14e9 --- /dev/null +++ b/packages/~/users/lib/src/usecase/SuggestSameUserEmails.test.ts @@ -0,0 +1,74 @@ +// + +import { + create_adora_pony_user, + create_pink_diamond_user, + create_red_diamond_user, + create_unicorn_organization, +} from "@~/moncomptepro.database/seed/unicorn"; +import { + add_user_to_organization, + empty_database, + migrate, + pg, +} from "@~/moncomptepro.database/testing"; +import { beforeAll, beforeEach, expect, test } from "bun:test"; +import { SuggestSameUserEmails } from "./SuggestSameUserEmails"; + +// + +beforeAll(migrate); +beforeEach(empty_database); + +const suggest_same_user_emails = SuggestSameUserEmails({ pg }); + +// + +test("returns all members", async () => { + const unicorn_organization_id = await given_unicorn_organization(); + const emails = await suggest_same_user_emails({ + organization_id: unicorn_organization_id, + family_name: "🧟", + }); + + expect(emails).toEqual([ + "adora.pony@unicorn.xyz", + "pink.diamond@unicorn.xyz", + "red.diamond@unicorn.xyz", + ]); +}); + +test("returns Diamond members", async () => { + const unicorn_organization_id = await given_unicorn_organization(); + const emails = await suggest_same_user_emails({ + organization_id: unicorn_organization_id, + family_name: "Diamond", + }); + + expect(emails).toEqual([ + "pink.diamond@unicorn.xyz", + "red.diamond@unicorn.xyz", + ]); +}); + +// + +async function given_unicorn_organization() { + const unicorn_organization_id = await create_unicorn_organization(pg); + const adora_pony_user_id = await create_adora_pony_user(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: adora_pony_user_id, + }); + const pink_diamond_user_id = await create_pink_diamond_user(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: pink_diamond_user_id, + }); + const red_diamond_user_id = await create_red_diamond_user(pg); + await add_user_to_organization({ + organization_id: unicorn_organization_id, + user_id: red_diamond_user_id, + }); + return unicorn_organization_id; +} diff --git a/packages/~/users/lib/src/usecase/SuggestSameUserEmails.ts b/packages/~/users/lib/src/usecase/SuggestSameUserEmails.ts new file mode 100644 index 00000000..e6beff46 --- /dev/null +++ b/packages/~/users/lib/src/usecase/SuggestSameUserEmails.ts @@ -0,0 +1,57 @@ +// + +import type { Simplify } from "@~/app.core/types"; +import { + schema, + type MonCompteProDatabaseCradle, + type User, + type Users_Organizations, +} from "@~/moncomptepro.database"; +import { and, eq, ilike } from "drizzle-orm"; + +// + +type Pattern = Simplify< + Pick & Pick +>; +export function SuggestSameUserEmails({ pg }: MonCompteProDatabaseCradle) { + return async function suggest_same_user_emails(pattern: Pattern) { + const { family_name, organization_id } = pattern; + const same_family_name_members = await pg + .select({ email: schema.users.email }) + .from(schema.users) + .innerJoin( + schema.users_organizations, + eq(schema.users.id, schema.users_organizations.user_id), + ) + .where( + and( + eq(schema.users_organizations.organization_id, organization_id), + family_name + ? ilike(schema.users.family_name, family_name) + : undefined, + ), + ); + + if (same_family_name_members.length > 0) + return same_family_name_members.map(({ email }) => email); + + const users = await pg + .select({ email: schema.users.email }) + .from(schema.users) + .innerJoin( + schema.users_organizations, + eq(schema.users.id, schema.users_organizations.user_id), + ) + .where(eq(schema.users_organizations.organization_id, organization_id)); + + return users.map(({ email }) => email); + }; +} + +export type SuggestSameUserEmailsHandler = ReturnType< + typeof SuggestSameUserEmails +>; +export type SuggestSameUserEmailsOutput = Awaited< + ReturnType +>; diff --git a/packages/~/users/ui/package.json b/packages/~/users/ui/package.json new file mode 100644 index 00000000..79cac8b7 --- /dev/null +++ b/packages/~/users/ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "@~/users.ui", + "version": "1.0.0", + "private": true, + "type": "module", + "imports": { + "#ui/*": { + "types": "./src/*/index.ts", + "default": "./src/*/index.ts" + } + }, + "exports": { + "./*": { + "types": "./src/*/index.ts", + "default": "./src/*/index.ts" + } + }, + "dependencies": { + "@~/app.ui": "workspace:*", + "@~/app.urls": "workspace:*", + "@~/organizations.lib": "workspace:*", + "@~/users.lib": "workspace:*", + "@~/moderations.lib": "workspace:*", + "hono": "4.6.3", + "tailwind-variants": "0.2.1", + "ts-pattern": "5.4.0" + }, + "devDependencies": { + "@~/crisp.lib": "workspace:*", + "@~/config.typescript": "workspace:*" + } +} diff --git a/packages/~/moderations/api/src/:id/About_User.tsx b/packages/~/users/ui/src/About/About.tsx similarity index 55% rename from packages/~/moderations/api/src/:id/About_User.tsx rename to packages/~/users/ui/src/About/About.tsx index f5dc054c..f721afa4 100644 --- a/packages/~/moderations/api/src/:id/About_User.tsx +++ b/packages/~/users/ui/src/About/About.tsx @@ -1,23 +1,20 @@ // import { z_email_domain } from "@~/app.core/schema/z_email_domain"; -import { button } from "@~/app.ui/button"; import { CopyButton } from "@~/app.ui/button/components/copy"; -import { GoogleSearchButton } from "@~/app.ui/button/components/search"; import { LocalTime } from "@~/app.ui/time/LocalTime"; import { urls } from "@~/app.urls"; -import { type JSX } from "hono/jsx"; -import { usePageRequestContext } from "./context"; +import type { GetUserInfoOutput } from "@~/users.lib/usecase/GetUserInfo"; // -export function About_User() { - const { - var: { - moderation: { created_at: moderation_created_at, user }, - }, - } = usePageRequestContext(); +type AboutProps = { + user: GetUserInfoOutput; +}; +// + +export function About({ user }: AboutProps) { const domain = z_email_domain.parse(user.email, { path: ["user.email"] }); return ( @@ -76,12 +73,12 @@ export function About_User() { -
  • + {/*
  • Demande de création :{" "} -
  • + */}
  • Nombre de connection : {user.sign_in_count}
  • @@ -90,46 +87,3 @@ export function About_User() { ); } - -export function Investigation_User(props: JSX.IntrinsicElements["section"]) { - const { - var: { - moderation: { user, organization }, - }, - } = usePageRequestContext(); - - const domain = z_email_domain.parse(user.email, { path: ["user.email"] }); - - return ( -
    -

    🕵️ Enquête sur ce profile

    - -
      -
    • - - Résultats Google pour cet email - -
    • -
    • - - Résultats Google pour ce nom de domaine - -
    • -
    • - - Résultats Google pour le nom de l'organisation et le nom de domaine - -
    • -
    -
    - ); -} diff --git a/packages/~/users/ui/src/About/index.ts b/packages/~/users/ui/src/About/index.ts new file mode 100644 index 00000000..f8ec6918 --- /dev/null +++ b/packages/~/users/ui/src/About/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./About"; diff --git a/packages/~/users/ui/src/Investigation/Investigation.tsx b/packages/~/users/ui/src/Investigation/Investigation.tsx new file mode 100644 index 00000000..aeeddf86 --- /dev/null +++ b/packages/~/users/ui/src/Investigation/Investigation.tsx @@ -0,0 +1,55 @@ +// + +import { z_email_domain } from "@~/app.core/schema/z_email_domain"; +import { button } from "@~/app.ui/button"; +import { GoogleSearchButton } from "@~/app.ui/button/components/search"; +import type { Organization } from "@~/organizations.lib/entities/Organization"; +import type { User } from "@~/users.lib/entities/User"; + +// + +type InvestigationProps = { + user: Pick; + organization: Pick; +}; + +// + +export function Investigation(props: InvestigationProps) { + const { user, organization } = props; + + const domain = z_email_domain.parse(user.email, { path: ["user.email"] }); + + return ( +
    +

    🕵️ Enquête sur ce profile

    + +
      +
    • + + Résultats Google pour cet email + +
    • +
    • + + Résultats Google pour ce nom de domaine + +
    • +
    • + + Résultats Google pour le nom de l'organisation et le nom de domaine + +
    • +
    +
    + ); +} diff --git a/packages/~/users/ui/src/Investigation/index.ts b/packages/~/users/ui/src/Investigation/index.ts new file mode 100644 index 00000000..9be65b42 --- /dev/null +++ b/packages/~/users/ui/src/Investigation/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./Investigation"; diff --git a/packages/~/users/ui/tsconfig.json b/packages/~/users/ui/tsconfig.json new file mode 100644 index 00000000..888b4ebf --- /dev/null +++ b/packages/~/users/ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "jsxImportSource": "hono/jsx", + "outDir": "./node_modules/.cache/tsc" + }, + "extends": "@~/config.typescript/api/tsconfig.json", + "include": ["src"], + "references": [ + { + "path": "../../app/ui" + }, + { + "path": "../../app/urls" + }, + { + "path": "../lib" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 07d18a2c..5a56cccf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "build.ts", "packages/~/*/*/*/src/**/*.tsx", "packages/~/*/*/src/**/*.tsx" - ], +, "packages/~/moderations/ui/src/Actions/context.ts" ], "references": [ { "path": "packages/~/app/api/tsconfig.json" }, { "path": "packages/~/app/core/tsconfig.json" }
    Date de création