initial commit

This commit is contained in:
Danel Kungla
2025-08-28 13:15:39 +03:00
parent 5159325e6d
commit 31bc4b6cff
11 changed files with 345 additions and 151 deletions

View File

@@ -71,19 +71,19 @@ export default async function syncConnectedOnline() {
return { return {
id: service.ID, id: service.ID,
clinic_id: service.ClinicID, clinic_id: service.ClinicID,
code: service.Code, sync_id: service.SyncID,
description: service.Description || null,
display: service.Display,
duration: service.Duration,
has_free_codes: !!service.HasFreeCodes,
name: service.Name, name: service.Name,
description: service.Description || null,
price: service.Price,
requires_payment: !!service.RequiresPayment,
duration: service.Duration,
neto_duration: service.NetoDuration, neto_duration: service.NetoDuration,
display: service.Display,
price_periods: service.PricePeriods || null,
online_hide_duration: service.OnlineHideDuration, online_hide_duration: service.OnlineHideDuration,
online_hide_price: service.OnlineHidePrice, online_hide_price: service.OnlineHidePrice,
price: service.Price, code: service.Code,
price_periods: service.PricePeriods || null, has_free_codes: !!service.HasFreeCodes,
requires_payment: !!service.RequiresPayment,
sync_id: service.SyncID,
}; };
}); });

View File

@@ -0,0 +1,33 @@
import { use } from 'react';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeLayoutPageHeader } from '../../../_components/home-page-header';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('booking:title');
return {
title,
};
};
function BookingHandlePage() {
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />}
/>
<PageBody></PageBody>
</>
);
}
export default withI18n(BookingHandlePage);

View File

@@ -1,12 +1,15 @@
import { use } from 'react';
import { PageBody } from '@kit/ui/page'; import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeLayoutPageHeader } from '../../_components/home-page-header'; import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import OrderCards from '../../_components/order-cards'; import OrderCards from '../../_components/order-cards';
import ServiceCategories from '../../_components/service-categories';
import { loadTtoServices } from '../../_lib/server/load-tto-services';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -18,6 +21,8 @@ export const generateMetadata = async () => {
}; };
function BookingPage() { function BookingPage() {
const { heroCategories, ttoCategories } = use(loadTtoServices());
console.log('ttoCategories', heroCategories, ttoCategories);
return ( return (
<> <>
<HomeLayoutPageHeader <HomeLayoutPageHeader
@@ -26,7 +31,8 @@ function BookingPage() {
/> />
<PageBody> <PageBody>
<OrderCards /> <OrderCards heroCategories={heroCategories} />
<ServiceCategories categories={ttoCategories} />
</PageBody> </PageBody>
</> </>
); );

View File

@@ -1,74 +1,68 @@
"use client"; 'use client';
import { ChevronRight, HeartPulse } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { cn } from '@/lib/utils';
import { pathsConfig } from '@/packages/shared/src/config';
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
import { ChevronRight, HeartPulse } from 'lucide-react';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
Card, Card,
CardHeader,
CardDescription, CardDescription,
CardProps,
CardFooter, CardFooter,
CardHeader,
CardProps,
} from '@kit/ui/card'; } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@/lib/utils';
const dummyCards = [ import { ServiceCategory } from './service-categories';
{
title: 'booking:analysisPackages.title',
description: 'booking:analysisPackages.description',
descriptionColor: 'text-primary',
icon: (
<Link href={'/home/order-analysis-package'}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
),
cardVariant: 'gradient-success' as CardProps['variant'],
iconBg: 'bg-warning',
},
];
export default function OrderCards() { export default function OrderCards({
heroCategories,
}: {
heroCategories: ServiceCategory[];
}) {
return ( return (
<div className="grid grid-cols-3 gap-6 mt-4"> <div className="mt-4 grid grid-cols-3 gap-6">
{dummyCards.map(({ {heroCategories.map(({ name, description, color, handle }) => (
title,
description,
icon,
cardVariant,
descriptionColor,
iconBg,
}) => (
<Card <Card
key={title} key={name}
variant={cardVariant} variant={`gradient-${color}` as CardProps['variant']}
className="flex flex-col justify-between" className="flex flex-col justify-between"
> >
<CardHeader className="items-end-safe"> <CardHeader className="relative flex flex-row justify-between">
<div <div
className={cn( className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white', 'flex size-8 items-center-safe justify-center-safe rounded-full',
iconBg, `text-${color}`,
`bg-${color}/10`,
{
'bg-primary/10': color === 'success',
},
)} )}
> >
{icon} <ComponentInstanceIcon
className={cn('size-4', `fill-${color}`)}
/>
</div>
<div className="absolute top-2 right-2 flex size-8 items-center-safe justify-center-safe rounded-xl text-white">
<Link
href={pathsConfig.app.bookingHandle.replace('[handle]', handle)}
>
<Button
size="icon"
variant="outline"
className="px-2 text-black"
>
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
</div> </div>
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-start gap-2"> <CardFooter className="mt-5 flex flex-col items-start gap-2">
<div <h5>{name}</h5>
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'} <CardDescription>{description}</CardDescription>
>
<HeartPulse className="size-4 fill-green-500" />
</div>
<h5>
<Trans i18nKey={title} />
</h5>
<CardDescription className={descriptionColor}>
<Trans i18nKey={description} />
</CardDescription>
</CardFooter> </CardFooter>
</Card> </Card>
))} ))}

View File

@@ -0,0 +1,61 @@
'use client';
import React from 'react';
import { redirect } from 'next/navigation';
import { createPath, pathsConfig } from '@/packages/shared/src/config';
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
import { cn } from '@kit/ui/shadcn';
import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card';
export interface ServiceCategory {
name: string;
handle: string;
color: string;
description: string;
}
const ServiceCategories = ({
categories,
}: {
categories: ServiceCategory[];
}) => {
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
{categories.map((category, index) => (
<Card
key={index}
className="flex cursor-pointer gap-2 p-4 shadow hover:shadow-md"
onClick={() => {
redirect(
pathsConfig.app.bookingHandle.replace(
'[handle]',
category.handle,
),
);
}}
>
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full',
`bg-${category.color}/10`,
`text-${category.color}`,
)}
>
<ComponentInstanceIcon />
</div>
<div>
<h5 className="mb-2 text-lg font-semibold">{category.name}</h5>
<CardDescription className="">
{category.description}
</CardDescription>
</div>
</Card>
))}
</div>
);
};
export default ServiceCategories;

View File

@@ -1,9 +1,11 @@
import { cache } from 'react'; import { cache } from 'react';
import { listProductTypes } from "@lib/data/products";
import { listRegions } from '@lib/data/regions';
import { getProductCategories } from '@lib/data/categories'; import { getProductCategories } from '@lib/data/categories';
import { listProductTypes } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
import { ServiceCategory } from '../../_components/service-categories';
async function countryCodesLoader() { async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) => const countryCodes = await listRegions().then((regions) =>
@@ -14,7 +16,9 @@ async function countryCodesLoader() {
export const loadCountryCodes = cache(countryCodesLoader); export const loadCountryCodes = cache(countryCodesLoader);
async function productCategoriesLoader() { async function productCategoriesLoader() {
const productCategories = await getProductCategories({ fields: "*products, *products.variants" }); const productCategories = await getProductCategories({
fields: '*products, *products.variants, is_active',
});
return productCategories.product_categories ?? []; return productCategories.product_categories ?? [];
} }
export const loadProductCategories = cache(productCategoriesLoader); export const loadProductCategories = cache(productCategoriesLoader);
@@ -32,22 +36,31 @@ async function analysesLoader() {
]); ]);
const countryCode = countryCodes[0]!; const countryCode = countryCodes[0]!;
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis'); const category = productCategories.find(
({ metadata }) => metadata?.page === 'order-analysis',
);
const serviceCategories = productCategories.filter(
({ parent_category }) => parent_category?.handle === 'tto-categories',
);
console.log('serviceCategories', serviceCategories);
return { return {
analyses: category?.products?.map<OrderAnalysisCard>(({ title, description, subtitle, variants, status, metadata }) => { analyses:
const variant = variants![0]!; category?.products?.map<OrderAnalysisCard>(
return { ({ title, description, subtitle, variants, status, metadata }) => {
title, const variant = variants![0]!;
description, return {
subtitle, title,
variant: { description,
id: variant.id, subtitle,
variant: {
id: variant.id,
},
isAvailable:
status === 'published' && !!metadata?.analysisIdOriginal,
};
}, },
isAvailable: status === 'published' && !!metadata?.analysisIdOriginal, ) ?? [],
};
}) ?? [],
countryCode, countryCode,
} };
} }
export const loadAnalyses = cache(analysesLoader); export const loadAnalyses = cache(analysesLoader);

View File

@@ -0,0 +1,49 @@
import { cache } from 'react';
import { getProductCategories } from '@lib/data';
import { ServiceCategory } from '../../_components/service-categories';
async function ttoServicesLoader() {
const response = await getProductCategories({
fields: '*products, is_active, metadata',
});
console.log('response.product_categories', response.product_categories);
const heroCategories = response.product_categories?.filter(
({ parent_category, is_active, metadata }) =>
parent_category?.handle === 'tto-categories' &&
is_active &&
metadata?.isHero,
);
const ttoCategories = response.product_categories?.filter(
({ parent_category, is_active, metadata }) =>
parent_category?.handle === 'tto-categories' &&
is_active &&
!metadata?.isHero,
);
return {
heroCategories:
heroCategories.map<ServiceCategory>(
({ name, handle, metadata, description }) => ({
name,
handle,
color:
typeof metadata?.color === 'string' ? metadata.color : 'primary',
description,
}),
) ?? [],
ttoCategories:
ttoCategories.map<ServiceCategory>(
({ name, handle, metadata, description }) => ({
name,
handle,
color:
typeof metadata?.color === 'string' ? metadata.color : 'primary',
description,
}),
) ?? [],
};
}
export const loadTtoServices = cache(ttoServicesLoader);

View File

@@ -1,13 +1,13 @@
import { sdk } from "@lib/config" import { sdk } from "@lib/config";
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types";
import { getCacheOptions } from "./cookies" import { getCacheOptions } from "./cookies";
export const listCategories = async (query?: Record<string, any>) => { export const listCategories = async (query?: Record<string, any>) => {
const next = { const next = {
...(await getCacheOptions("categories")), ...(await getCacheOptions("categories")),
} };
const limit = query?.limit || 100 const limit = query?.limit || 100;
return sdk.client return sdk.client
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>( .fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
@@ -23,8 +23,8 @@ export const listCategories = async (query?: Record<string, any>) => {
cache: "force-cache", cache: "force-cache",
} }
) )
.then(({ product_categories }) => product_categories) .then(({ product_categories }) => product_categories);
} };
export const getCategoryByHandle = async (categoryHandle: string[]) => { export const getCategoryByHandle = async (categoryHandle: string[]) => {
const { product_categories } = await getProductCategories({ const { product_categories } = await getProductCategories({
@@ -32,7 +32,7 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => {
limit: 1, limit: 1,
}); });
return product_categories[0]; return product_categories[0];
} };
export const getProductCategories = async ({ export const getProductCategories = async ({
handle, handle,
@@ -45,19 +45,18 @@ export const getProductCategories = async ({
} = {}) => { } = {}) => {
const next = { const next = {
...(await getCacheOptions("categories")), ...(await getCacheOptions("categories")),
} };
return sdk.client return sdk.client.fetch<HttpTypes.StoreProductCategoryListResponse>(
.fetch<HttpTypes.StoreProductCategoryListResponse>( `/store/product-categories`,
`/store/product-categories`, {
{ query: {
query: { fields,
fields, handle,
handle, limit,
limit, },
}, next,
next, //cache: "force-cache",
//cache: "force-cache", }
} );
); };
}

View File

@@ -16,6 +16,7 @@ const PathsSchema = z.object({
home: z.string().min(1), home: z.string().min(1),
selectPackage: z.string().min(1), selectPackage: z.string().min(1),
booking: z.string().min(1), booking: z.string().min(1),
bookingHandle: z.string().min(1),
myOrders: z.string().min(1), myOrders: z.string().min(1),
analysisResults: z.string().min(1), analysisResults: z.string().min(1),
orderAnalysisPackage: z.string().min(1), orderAnalysisPackage: z.string().min(1),
@@ -64,6 +65,7 @@ const pathsConfig = PathsSchema.parse({
joinTeam: '/join', joinTeam: '/join',
selectPackage: '/select-package', selectPackage: '/select-package',
booking: '/home/booking', booking: '/home/booking',
bookingHandle: '/home/booking/[handle]',
orderAnalysisPackage: '/home/order-analysis-package', orderAnalysisPackage: '/home/order-analysis-package',
myOrders: '/home/order', myOrders: '/home/order',
analysisResults: '/home/analysis-results', analysisResults: '/home/analysis-results',

View File

@@ -108,6 +108,63 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
medipost_dispatch: {
Row: {
changed_by: string | null
created_at: string
error_message: string | null
id: number
is_medipost_error: boolean
is_success: boolean
medusa_order_id: string
}
Insert: {
changed_by?: string | null
created_at?: string
error_message?: string | null
id?: number
is_medipost_error: boolean
is_success: boolean
medusa_order_id: string
}
Update: {
changed_by?: string | null
created_at?: string
error_message?: string | null
id?: number
is_medipost_error?: boolean
is_success?: boolean
medusa_order_id?: string
}
Relationships: []
}
medusa_action: {
Row: {
action: string
created_at: string
id: number
medusa_user_id: string
page: string | null
user_email: string
}
Insert: {
action: string
created_at?: string
id?: number
medusa_user_id: string
page?: string | null
user_email: string
}
Update: {
action?: string
created_at?: string
id?: number
medusa_user_id?: string
page?: string | null
user_email?: string
}
Relationships: []
}
page_views: { page_views: {
Row: { Row: {
account_id: string account_id: string
@@ -201,28 +258,6 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
medusa_action: {
Row: {
id: number
medusa_user_id: string
user_email: string
action: string
page: string
created_at: string
}
Insert: {
medusa_user_id: string
user_email: string
action: string
page: string
}
Update: {
medusa_user_id?: string
user_email?: string
action?: string
page?: string
}
}
} }
Views: { Views: {
[_ in never]: never [_ in never]: never
@@ -329,6 +364,7 @@ export type Database = {
id: string id: string
is_personal_account: boolean is_personal_account: boolean
last_name: string | null last_name: string | null
medusa_account_id: string | null
name: string name: string
personal_code: string | null personal_code: string | null
phone: string | null phone: string | null
@@ -336,7 +372,6 @@ export type Database = {
primary_owner_user_id: string primary_owner_user_id: string
public_data: Json public_data: Json
slug: string | null slug: string | null
medusa_account_id: string | null
updated_at: string | null updated_at: string | null
updated_by: string | null updated_by: string | null
} }
@@ -351,6 +386,7 @@ export type Database = {
id?: string id?: string
is_personal_account?: boolean is_personal_account?: boolean
last_name?: string | null last_name?: string | null
medusa_account_id?: string | null
name: string name: string
personal_code?: string | null personal_code?: string | null
phone?: string | null phone?: string | null
@@ -358,7 +394,6 @@ export type Database = {
primary_owner_user_id?: string primary_owner_user_id?: string
public_data?: Json public_data?: Json
slug?: string | null slug?: string | null
medusa_account_id?: string | null
updated_at?: string | null updated_at?: string | null
updated_by?: string | null updated_by?: string | null
} }
@@ -373,6 +408,7 @@ export type Database = {
id?: string id?: string
is_personal_account?: boolean is_personal_account?: boolean
last_name?: string | null last_name?: string | null
medusa_account_id?: string | null
name?: string name?: string
personal_code?: string | null personal_code?: string | null
phone?: string | null phone?: string | null
@@ -380,7 +416,6 @@ export type Database = {
primary_owner_user_id?: string primary_owner_user_id?: string
public_data?: Json public_data?: Json
slug?: string | null slug?: string | null
medusa_account_id?: string | null
updated_at?: string | null updated_at?: string | null
updated_by?: string | null updated_by?: string | null
} }
@@ -393,6 +428,7 @@ export type Database = {
created_at: string created_at: string
created_by: string | null created_by: string | null
has_seen_confirmation: boolean has_seen_confirmation: boolean
id: string
updated_at: string updated_at: string
updated_by: string | null updated_by: string | null
user_id: string user_id: string
@@ -403,6 +439,7 @@ export type Database = {
created_at?: string created_at?: string
created_by?: string | null created_by?: string | null
has_seen_confirmation?: boolean has_seen_confirmation?: boolean
id?: string
updated_at?: string updated_at?: string
updated_by?: string | null updated_by?: string | null
user_id: string user_id: string
@@ -413,6 +450,7 @@ export type Database = {
created_at?: string created_at?: string
created_by?: string | null created_by?: string | null
has_seen_confirmation?: boolean has_seen_confirmation?: boolean
id?: string
updated_at?: string updated_at?: string
updated_by?: string | null updated_by?: string | null
user_id?: string user_id?: string
@@ -1022,7 +1060,7 @@ export type Database = {
price: number price: number
price_periods: string | null price_periods: string | null
requires_payment: boolean requires_payment: boolean
sync_id: number sync_id: string | null
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
@@ -1041,7 +1079,7 @@ export type Database = {
price: number price: number
price_periods?: string | null price_periods?: string | null
requires_payment: boolean requires_payment: boolean
sync_id: number sync_id?: string | null
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
@@ -1060,7 +1098,7 @@ export type Database = {
price?: number price?: number
price_periods?: string | null price_periods?: string | null
requires_payment?: boolean requires_payment?: boolean
sync_id?: number sync_id?: string | null
updated_at?: string | null updated_at?: string | null
} }
Relationships: [ Relationships: [
@@ -1081,7 +1119,7 @@ export type Database = {
doctor_user_id: string | null doctor_user_id: string | null
id: number id: number
status: Database["medreport"]["Enums"]["analysis_feedback_status"] status: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at: string updated_at: string | null
updated_by: string | null updated_by: string | null
user_id: string user_id: string
value: string | null value: string | null
@@ -1093,7 +1131,7 @@ export type Database = {
doctor_user_id?: string | null doctor_user_id?: string | null
id?: number id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"] status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string updated_at?: string | null
updated_by?: string | null updated_by?: string | null
user_id: string user_id: string
value?: string | null value?: string | null
@@ -1105,7 +1143,7 @@ export type Database = {
doctor_user_id?: string | null doctor_user_id?: string | null
id?: number id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"] status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string updated_at?: string | null
updated_by?: string | null updated_by?: string | null
user_id?: string user_id?: string
value?: string | null value?: string | null
@@ -1784,9 +1822,7 @@ export type Database = {
Returns: Json Returns: Json
} }
create_team_account: { create_team_account: {
Args: Args: { account_name: string; new_personal_code: string }
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: { Returns: {
application_role: Database["medreport"]["Enums"]["application_role"] application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null city: string | null
@@ -1798,6 +1834,7 @@ export type Database = {
id: string id: string
is_personal_account: boolean is_personal_account: boolean
last_name: string | null last_name: string | null
medusa_account_id: string | null
name: string name: string
personal_code: string | null personal_code: string | null
phone: string | null phone: string | null
@@ -1836,6 +1873,7 @@ export type Database = {
primary_owner_user_id: string primary_owner_user_id: string
name: string name: string
email: string email: string
personal_code: string
picture_url: string picture_url: string
created_at: string created_at: string
updated_at: string updated_at: string
@@ -1853,10 +1891,18 @@ export type Database = {
account_id: string account_id: string
}[] }[]
} }
get_medipost_dispatch_tries: {
Args: { p_medusa_order_id: string }
Returns: number
}
get_nonce_status: { get_nonce_status: {
Args: { p_id: string } Args: { p_id: string }
Returns: Json Returns: Json
} }
get_order_possible_actions: {
Args: { p_medusa_order_id: string }
Returns: Json
}
get_upper_system_role: { get_upper_system_role: {
Args: Record<PropertyKey, never> Args: Record<PropertyKey, never>
Returns: string Returns: string
@@ -1937,6 +1983,10 @@ export type Database = {
Args: { account_id: string; user_id: string } Args: { account_id: string; user_id: string }
Returns: boolean Returns: boolean
} }
medipost_retry_dispatch: {
Args: { order_id: string }
Returns: Json
}
revoke_nonce: { revoke_nonce: {
Args: { p_id: string; p_reason?: string } Args: { p_id: string; p_reason?: string }
Returns: boolean Returns: boolean
@@ -2057,21 +2107,6 @@ export type Database = {
} }
Returns: Json Returns: Json
} }
medipost_retry_dispatch: {
Args: {
order_id: string
}
Returns: {
success: boolean
error: string | null
}
}
get_medipost_dispatch_tries: {
Args: {
p_medusa_order_id: string
}
Returns: number
}
} }
Enums: { Enums: {
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED" analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"

View File

@@ -0,0 +1,2 @@
ALTER TABLE medreport.connected_online_services
ALTER COLUMN sync_id TYPE text USING sync_id::text;