diff --git a/apps/infoalloggi/package-lock.json b/apps/infoalloggi/package-lock.json index 4af3ddb..d40e1ab 100644 --- a/apps/infoalloggi/package-lock.json +++ b/apps/infoalloggi/package-lock.json @@ -8,6 +8,9 @@ "name": "infoalloggi", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fattureincloud/fattureincloud-ts-sdk": "^2.1.1", "@hookform/resolvers": "^5.2.1", "@next/bundle-analyzer": "^16.0.2", @@ -1227,6 +1230,60 @@ "node": ">=10.0.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@electric-sql/pglite": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.7.tgz", diff --git a/apps/infoalloggi/package.json b/apps/infoalloggi/package.json index c2863ba..04da2bf 100644 --- a/apps/infoalloggi/package.json +++ b/apps/infoalloggi/package.json @@ -20,6 +20,9 @@ }, "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fattureincloud/fattureincloud-ts-sdk": "^2.1.1", "@hookform/resolvers": "^5.2.1", "@next/bundle-analyzer": "^16.0.2", diff --git a/apps/infoalloggi/src/components/area-riservata/potenziali.tsx b/apps/infoalloggi/src/components/area-riservata/potenziali.tsx new file mode 100644 index 0000000..c047416 --- /dev/null +++ b/apps/infoalloggi/src/components/area-riservata/potenziali.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { format } from "date-fns"; +import { + KanbanBoard, + KanbanCard, + KanbanCards, + KanbanHeader, + KanbanProvider, +} from "~/components/ui/kanban"; +import { + type ElementoPotenziali, + PotenzialiProvider, + usePotenziali, +} from "~/providers/PotenzialiProvider"; +import type { UsersId } from "~/schemas/public/Users"; +import { + Credenza, + CredenzaBody, + CredenzaContent, + CredenzaDescription, + CredenzaHeader, + CredenzaTitle, +} from "../custom_ui/credenza"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../ui/context-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { UserAvatar } from "../user_avatar"; + +const columns = [ + { id: "todo", name: "To Do", color: "red" }, + { id: "inprogress", name: "In Progress", color: "blue" }, + { id: "done", name: "Done", color: "green" }, +]; + +const SampleData: ElementoPotenziali[] = [ + { + id: "1", + name: "Mario Rossi", + column: "todo", + descrizione: + "Single, cerca appartamento in centro da subito, budget 800 euro", + createdAt: new Date(), + owner: { + userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId, + username: "Marco Pedone", + }, + }, + { + id: "2", + name: "Luigi Bianchi", + column: "inprogress", + descrizione: "Coppia, cerca bilocale in periferia, budget 600 euro", + createdAt: new Date(), + owner: null, + }, + { + id: "3", + name: "Giovanni Verdi", + column: "done", + descrizione: + "Famiglia con 2 figli, cerca trilocale vicino a scuole, budget 1000 euro", + createdAt: new Date(), + owner: { + userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId, + username: "Marco Pedone", + }, + }, +]; + +const Potenziali = () => { + return ( + + + + ); +}; + +const Kaban = () => { + const { data, updateData, setEditingId, editingId } = usePotenziali(); + + return ( + <> + + {(column) => ( + + +
+
+ {column.name} +
+ + + {(data: ElementoPotenziali) => ( + { + setEditingId(data.id); + }} + column={column.id} + id={data.id} + key={data.id} + name={data.name} + > +
+
+

+ {data.name} +

+

+ {data.descrizione} +

+
+ {data.owner && ( + + )} +
+

+ {format(data.createdAt, "dd/MM/yyyy HH:mm")} +

+
+ )} +
+ + )} + + { + if (!v) { + setEditingId(null); + } + }} + open={!!editingId} + > + + + + ); +}; + +const EditingContent = () => { + const { getEditingItem } = usePotenziali(); + const item = getEditingItem(); + if (!item) return null; + return ( + + + + Modifica {item.name} + + desc + + +

Descrizione: {item.descrizione}

+

Creato il: {format(item.createdAt, "dd/MM/yyyy HH:mm")}

+

Utente Assegnato: {item.owner ? item.owner.username : "Nessuno"}

+
+
+ ); +}; + +export default Potenziali; diff --git a/apps/infoalloggi/src/components/ui/kanban.tsx b/apps/infoalloggi/src/components/ui/kanban.tsx new file mode 100644 index 0000000..cfac47a --- /dev/null +++ b/apps/infoalloggi/src/components/ui/kanban.tsx @@ -0,0 +1,369 @@ +"use client"; + +import type { + Announcements, + DndContextProps, + DragEndEvent, + DragOverEvent, + DragStartEvent, +} from "@dnd-kit/core"; +import { + closestCenter, + DndContext, + DragOverlay, + KeyboardSensor, + MouseSensor, + TouchSensor, + useDroppable, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripHorizontal } from "lucide-react"; +import { + createContext, + type HTMLAttributes, + type ReactNode, + useContext, + useState, +} from "react"; +import { createPortal } from "react-dom"; +import tunnel from "tunnel-rat"; +import { Card } from "~/components/ui/card"; +import { ScrollArea, ScrollBar } from "~/components/ui/scroll-area"; +import { cn } from "~/lib/utils"; + +//https://www.shadcn.io/components/data/kanban#installation +const t = tunnel(); + +export type { DragEndEvent } from "@dnd-kit/core"; + +type KanbanItemProps = { + id: string; + name: string; + column: string; +} & Record; + +type KanbanColumnProps = { + id: string; + name: string; +} & Record; + +type KanbanContextProps< + T extends KanbanItemProps = KanbanItemProps, + C extends KanbanColumnProps = KanbanColumnProps, +> = { + columns: C[]; + data: T[]; + activeCardId: string | null; +}; + +const KanbanContext = createContext({ + columns: [], + data: [], + activeCardId: null, +}); + +export type KanbanBoardProps = { + id: string; + children: ReactNode; + className?: string; +}; + +export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => { + const { isOver, setNodeRef } = useDroppable({ + id, + }); + + return ( +
+ {children} +
+ ); +}; + +export type KanbanCardProps = T & { + children?: ReactNode; + className?: string; + cardOnClick?: () => void; +}; + +export const KanbanCard = ({ + id, + name, + children, + className, + cardOnClick, +}: KanbanCardProps) => { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transition, + transform, + isDragging, + } = useSortable({ + id, + }); + const { activeCardId } = useContext(KanbanContext) as KanbanContextProps; + + const style = { + transition, + transform: CSS.Transform.toString(transform), + }; + + return ( + <> +
+ +
+ +
+ +
+ {children ??

{name}

} +
+
+
+ + {activeCardId === id && ( + + +
+ +
+
+ {children ??

{name}

} +
+
+
+ )} + + ); +}; + +export type KanbanCardsProps = + Omit, "children" | "id"> & { + children: (item: T) => ReactNode; + id: string; + }; + +export const KanbanCards = ({ + children, + className, + ...props +}: KanbanCardsProps) => { + const { data } = useContext(KanbanContext) as KanbanContextProps; + const filteredData = data.filter((item) => item.column === props.id); + const items = filteredData.map((item) => item.id); + + return ( + + +
+ {filteredData.map(children)} +
+
+ +
+ ); +}; + +export type KanbanHeaderProps = HTMLAttributes; + +export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => ( +
+); + +export type KanbanProviderProps< + T extends KanbanItemProps = KanbanItemProps, + C extends KanbanColumnProps = KanbanColumnProps, +> = Omit & { + children: (column: C) => ReactNode; + className?: string; + columns: C[]; + data: T[]; + onDataChange?: (data: T[]) => void; + onDragStart?: (event: DragStartEvent) => void; + onDragEnd?: (event: DragEndEvent) => void; + onDragOver?: (event: DragOverEvent) => void; +}; + +export const KanbanProvider = < + T extends KanbanItemProps = KanbanItemProps, + C extends KanbanColumnProps = KanbanColumnProps, +>({ + children, + onDragStart, + onDragEnd, + onDragOver, + className, + columns, + data, + onDataChange, + ...props +}: KanbanProviderProps) => { + const [activeCardId, setActiveCardId] = useState(null); + + const sensors = useSensors( + useSensor(MouseSensor), + useSensor(TouchSensor), + useSensor(KeyboardSensor), + ); + + const handleDragStart = (event: DragStartEvent) => { + const card = data.find((item) => item.id === event.active.id); + if (card) { + setActiveCardId(event.active.id as string); + } + onDragStart?.(event); + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over } = event; + + if (!over) { + return; + } + + const activeItem = data.find((item) => item.id === active.id); + const overItem = data.find((item) => item.id === over.id); + + if (!activeItem) { + return; + } + + const activeColumn = activeItem.column; + const overColumn = + overItem?.column || + columns.find((col) => col.id === over.id)?.id || + columns[0]?.id; + + if (!overColumn) { + return; + } + + if (activeColumn !== overColumn) { + let newData = [...data]; + const activeIndex = newData.findIndex((item) => item.id === active.id); + const overIndex = newData.findIndex((item) => item.id === over.id); + + if (newData[activeIndex] === undefined) { + return; + } + newData[activeIndex].column = overColumn; // Now TypeScript knows this is a string + newData = arrayMove(newData, activeIndex, overIndex); + + onDataChange?.(newData); + } + + onDragOver?.(event); + }; + + const handleDragEnd = (event: DragEndEvent) => { + setActiveCardId(null); + + onDragEnd?.(event); + + const { active, over } = event; + + if (!over || active.id === over.id) { + return; + } + + let newData = [...data]; + + const oldIndex = newData.findIndex((item) => item.id === active.id); + const newIndex = newData.findIndex((item) => item.id === over.id); + + newData = arrayMove(newData, oldIndex, newIndex); + + onDataChange?.(newData); + }; + + const announcements: Announcements = { + onDragStart({ active }) { + const { name, column } = data.find((item) => item.id === active.id) ?? {}; + + return `Picked up the card "${name}" from the "${column}" column`; + }, + onDragOver({ active, over }) { + const { name } = data.find((item) => item.id === active.id) ?? {}; + const newColumn = columns.find((column) => column.id === over?.id)?.name; + + return `Dragged the card "${name}" over the "${newColumn}" column`; + }, + onDragEnd({ active, over }) { + const { name } = data.find((item) => item.id === active.id) ?? {}; + const newColumn = columns.find((column) => column.id === over?.id)?.name; + + return `Dropped the card "${name}" into the "${newColumn}" column`; + }, + onDragCancel({ active }) { + const { name } = data.find((item) => item.id === active.id) ?? {}; + + return `Cancelled dragging the card "${name}"`; + }, + }; + + return ( + + +
+ {columns.map((column) => children(column))} +
+ {typeof window !== "undefined" && + createPortal( + + + , + document.body, + )} +
+
+ ); +}; diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts index 15a6226..5be715c 100644 --- a/apps/infoalloggi/src/i18n/en.ts +++ b/apps/infoalloggi/src/i18n/en.ts @@ -605,6 +605,7 @@ export const en: LangDict = { { icon: "cog", items: [ + { href: "/area-riservata/admin/potenziali", title: "Potenziali" }, { href: "/area-riservata/admin/utenti", title: "Users" }, { href: "/area-riservata/admin/chats", title: "Chat List" }, { href: "/area-riservata/admin/ordini", title: "Orders" }, diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts index 726d7e3..8dd18f6 100644 --- a/apps/infoalloggi/src/i18n/it.ts +++ b/apps/infoalloggi/src/i18n/it.ts @@ -610,6 +610,7 @@ export const it: LangDict = { { icon: "cog", items: [ + { href: "/area-riservata/admin/potenziali", title: "Potenziali" }, { href: "/area-riservata/admin/utenti", title: "Utenti" }, { href: "/area-riservata/admin/chats", title: "Lista Chat" }, { href: "/area-riservata/admin/ordini", title: "Ordini" }, diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/potenziali.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/potenziali.tsx new file mode 100644 index 0000000..e7d2726 --- /dev/null +++ b/apps/infoalloggi/src/pages/area-riservata/admin/potenziali.tsx @@ -0,0 +1,53 @@ +import dynamic from "next/dynamic"; +import { AreaRiservataLayout } from "~/components/Layout"; +import { LoadingPage } from "~/components/loading"; +import type { NextPageWithLayout } from "~/pages/_app"; + +const Potenziali = dynamic( + () => import("~/components/area-riservata/potenziali"), + { + loading: () => , + ssr: false, + }, +); + +const AdminPotenziali: NextPageWithLayout = () => { + return ( +
+
+
+
+

+ Potenziali +

+
+
+
+ +
+
+
+ ); +}; +export default AdminPotenziali; + +// export const getServerSideProps = (async (context) => { +// const access_token = context.req.cookies.access_token; + +// const helpers = await TrpcAuthedFetchingIstance({ access_token }); +// if (helpers) { +// await helpers.trpc.etichette.getAllEtichette.prefetch(); +// return { +// props: { +// trpcState: helpers.trpc.dehydrate(), +// }, +// }; +// } +// return { +// props: {}, +// }; +// }) satisfies GetServerSideProps; + +AdminPotenziali.getLayout = function getLayout(page) { + return {page}; +}; diff --git a/apps/infoalloggi/src/providers/PotenzialiProvider.tsx b/apps/infoalloggi/src/providers/PotenzialiProvider.tsx new file mode 100644 index 0000000..100fcd4 --- /dev/null +++ b/apps/infoalloggi/src/providers/PotenzialiProvider.tsx @@ -0,0 +1,66 @@ +import { createContext, useContext, useState } from "react"; +import type { UsersId } from "~/schemas/public/Users"; + +export type ElementoPotenziali = { + id: string; + name: string; + column: string; + descrizione: string; + createdAt: Date; + owner: { userId: UsersId; username: string } | null; +}; +type PotenzialiContextType = { + data: ElementoPotenziali[]; + editingId: string | null; + updateData: (data: ElementoPotenziali[]) => void; + setEditingId: (id: string | null) => void; + getEditingItem: () => ElementoPotenziali | null; + addItem: (item: ElementoPotenziali) => void; + removeItem: (id: string) => void; +}; + +const PotenzialiContext = createContext(null); + +type PotenzialiProviderProps = { + children: React.ReactNode; + data: ElementoPotenziali[]; +}; +export const PotenzialiProvider = ({ + children, + data: initial, +}: PotenzialiProviderProps) => { + const [data, setData] = useState(initial); + const [editingId, setEditingId] = useState(null); + + const getEditingItem = () => { + return data.find((item) => item.id === editingId) || null; + }; + const addItem = (item: ElementoPotenziali) => { + setData((prev) => [...prev, item]); + }; + const removeItem = (id: string) => { + setData((prev) => prev.filter((item) => item.id !== id)); + }; + return ( + + {children} + + ); +}; +export const usePotenziali = () => { + const context = useContext(PotenzialiContext); + if (!context) { + throw new Error("usePotenziali must be used within a PotenzialiProvider"); + } + return context; +};