Merge branch 'dev' of https://devgit.oriyo.tj/adilovcode/oriyo_next into feat-get-media

This commit is contained in:
Umar Adilov 2025-05-14 20:05:00 +05:00
commit a6d836a543
19 changed files with 292 additions and 225 deletions

View File

@ -7,10 +7,18 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
ENV CI=true
WORKDIR /app
# Copy package.json and pnpm-lock.yaml first for caching
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install
# Copy the rest of the files
COPY . .
# Install dependencies and build
RUN pnpm install && pnpm build
# Build the application
RUN pnpm build
FROM node:18-alpine AS runner

View File

@ -22,49 +22,49 @@ export interface Select {
}
export type Discount = Root<{
_name: string;
_opisanie: string;
_do: string;
_foto: Image[];
zagolovok: string;
opisanie: string;
do: string;
foto: Image[];
}>;
export type Job = Root<{
id: number;
_name: string;
_type: Select[];
_localtio: Select[];
_tags: Select[];
zagolovok: string;
tip: Select[];
lokaciya: Select[];
tegi: Select[];
}>;
export type Partner = Root<{
id: number;
_name: string;
_image: Image[];
nazvanie: string;
izobrozhenie: Image[];
}>;
export type Station = Root<{
_name: string;
_opisanie: string;
_adress: string;
_chasyRaboty: Select;
_lat: number;
_long: number;
_avtomojka: boolean;
_dtCopy: boolean;
_avtomojkaCopy: boolean;
_ai92Copy: boolean;
_ai95Copy: boolean;
_z100Copy: boolean;
_propanCopy: boolean;
_zaryadnayaStanci: boolean;
_miniMarketCop: boolean;
_region: Select[];
_foto: Image[];
imya: string;
opisanie: string;
adress: string;
chasyraboty: Select[];
lat: number;
long: number;
avtomojka: boolean;
ai92: boolean;
ai95: boolean;
z100: boolean;
propan: boolean;
zaryadnayastanciya: boolean;
dt: boolean;
minimarket: boolean;
tualet: boolean;
region: Select[];
foto: Image[];
}>;
export type TextResponse = Root<{
_name: string;
_znachenie: string | null;
klyuchneizmenyat: string;
znachenie: string | null;
}>;
export type MediaResponse = Root<{
@ -74,38 +74,38 @@ export type MediaResponse = Root<{
}>;
export type Team = Root<{
_foto: Image[];
_zvanie: string;
_name: string;
foto: Image[];
zvanie: string;
polnoeimya: string;
}>;
export type History = Root<{
_name: string;
_god: string;
_opisanie: string;
zagolovok: string;
god: string;
opisanie: string;
}>;
export type Review = Root<{
id: number;
_name: string;
_otzyv: string;
_rejting: number;
polnoeimya: string;
otzyv: string;
rejting: number;
}>;
export type Charity = Root<{
_name: string;
_opisanie: string;
_data: string;
_lokaciya: string;
_foto: Image[];
zagolovok: string;
opisanie: string;
data: string;
lokaciya: string;
foto: Image[];
}>;
export type Certificate = Root<{
_name: string;
_opisanie: string;
_dataVydachi: string;
_dejstvitelenDo: string;
_foto: Image[];
nazvanie: string;
opisanie: string;
datavydachi: string;
dejstvitelenDo: string;
foto: Image[];
}>;
export type TeamMembers = ReturnType<typeof presentTeamMembers>;

View File

@ -12,6 +12,7 @@ export const requestTaylor = async (query: object, variables?: object) => {
headers: {
Authorization: process.env.TAYLOR_API_TOKEN || '',
'Content-type': 'application/json',
schema: 'readable',
},
});

View File

@ -0,0 +1 @@
export class AuthorizationError extends Error {}

View File

@ -25,68 +25,68 @@ export const presentSelect = (selectItems: Select[]) =>
export const presentPartners = (partners: Partner) =>
partners.records.map((record, index) => ({
id: index + 1,
name: record._name,
poster: presentImage(record._image),
name: record.nazvanie,
poster: presentImage(record.izobrozhenie),
}));
export const presentJobs = (jobs: Job) =>
jobs.records.map((job, index) => ({
id: index + 1,
name: job._name,
tags: job._tags.map((tag) => tag.name),
location: presentSelect(job._localtio),
type: presentSelect(job._type),
name: job.zagolovok,
tags: job.tegi.map((tag) => tag.name),
location: presentSelect(job.lokaciya),
type: presentSelect(job.tip),
}));
export const presentTeamMembers = (members: Team) =>
members.records.map((member) => ({
name: member._name,
photo: presentImage(member._foto),
profession: member._zvanie,
name: member.polnoeimya,
photo: presentImage(member.foto),
profession: member.zvanie,
}));
export const presentHistoryItems = (historyItems: History) =>
historyItems.records.map((item) => ({
name: item._name,
year: item._god,
description: item._opisanie,
name: item.zagolovok,
year: item.god,
description: item.opisanie,
}));
export const presentDiscounts = (discounts: Discount) =>
discounts.records.map((discount, index) => ({
id: index + 1,
name: discount._name,
description: discount._opisanie,
expiresAt: discount._do,
image: presentImage(discount._foto),
name: discount.zagolovok,
description: discount.opisanie,
expiresAt: discount.do,
image: presentImage(discount.foto),
}));
export const presentStations = (stations: Station) =>
stations.records.map((station, index) => ({
id: index + 1,
name: station._name,
description: station._opisanie,
address: station._adress,
workingHours: station._chasyRaboty?.name || null,
latitude: station._lat,
longitude: station._long,
carWash: station._avtomojka || false,
ai92: station._dtCopy || false,
ai95: station._ai92Copy || false,
dt: station._avtomojkaCopy || false,
z100: station._ai95Copy || false,
propan: station._z100Copy || false,
electricCharge: station._propanCopy || false,
miniMarket: station._zaryadnayaStanci || false,
toilet: station._miniMarketCop || false,
region: presentSelect(station._region),
image: presentImage(station._foto),
name: station.imya,
description: station.opisanie,
address: station.adress,
workingHours: presentSelect(station.chasyraboty),
latitude: station.lat,
longitude: station.long,
carWash: station.avtomojka || false,
ai92: station.ai92 || false,
ai95: station.ai95 || false,
dt: station.dt || false,
z100: station.z100 || false,
propan: station.propan || false,
electricCharge: station.zaryadnayastanciya || false,
miniMarket: station.minimarket || false,
toilet: station.tualet || false,
region: presentSelect(station.region),
image: presentImage(station.foto),
}));
export const presentTexts = (texts: TextResponse) =>
texts.records.map((item) => ({
key: item._name,
value: item._znachenie,
key: item.klyuchneizmenyat,
value: item.znachenie,
}));
export const presentMedia = (media: MediaResponse) => {
@ -100,27 +100,27 @@ export const presentMedia = (media: MediaResponse) => {
export const presentReviews = (reviews: Review) =>
reviews.records.map((review) => ({
id: review.id,
fullname: review._name,
review: review._otzyv,
rating: review._rejting,
fullname: review.polnoeimya,
review: review.otzyv,
rating: review.rejting,
}));
export const presentCharities = (charities: Charity) =>
charities.records.map((charity, index) => ({
id: index + 1,
name: charity._name,
description: charity._opisanie,
date: charity._data,
location: charity._lokaciya,
image: presentImage(charity._foto),
name: charity.zagolovok,
description: charity.opisanie,
date: charity.data,
location: charity.lokaciya,
image: presentImage(charity.foto),
}));
export const presentCertificates = (certificates: Certificate) =>
certificates.records.map((certificate, index) => ({
id: index + 1,
name: certificate._name,
description: certificate._opisanie,
issuedAt: certificate._dataVydachi,
validUntil: certificate._dejstvitelenDo,
image: presentImage(certificate._foto),
name: certificate.nazvanie,
description: certificate.opisanie,
issuedAt: certificate.datavydachi,
validUntil: certificate.dejstvitelenDo,
image: presentImage(certificate.foto),
}));

View File

@ -1,29 +1,29 @@
import { EnumType, VariableType } from 'json-to-graphql-query';
export const stationsRequest = {
_azs: {
azs: {
records: {
_name: true,
_opisanie: true,
_adress: true,
_chasyRaboty: {
imya: true,
opisanie: true,
adress: true,
chasyraboty: {
name: true,
},
_lat: true,
_long: true,
_avtomojka: true,
_dtCopy: true, // ai92
_ai92Copy: true, // ai95
_ai95Copy: true, // z100
_z100Copy: true, // propan
_propanCopy: true, // electricCharge
_avtomojkaCopy: true, // DT
_zaryadnayaStanci: true, // miniMarket
_miniMarketCop: true, // toilet
_region: {
lat: true,
long: true,
avtomojka: true,
ai92: true,
ai95: true,
z100: true,
propan: true,
zaryadnayastanciya: true,
dt: true,
minimarket: true,
tualet: true,
region: {
name: true,
},
_foto: {
foto: {
url: true,
},
},
@ -31,13 +31,13 @@ export const stationsRequest = {
};
export const stationsWithImageRequest = {
_azs: {
azs: {
__args: {
filtersSet: {
conjunction: new EnumType('and'),
filtersSet: [
{
field: new EnumType('_foto'),
field: new EnumType('foto'),
operator: 'isNotEmpty',
value: [],
},
@ -46,26 +46,27 @@ export const stationsWithImageRequest = {
},
records: {
_name: true,
_opisanie: true,
_adress: true,
_chasyRaboty: {
imya: true,
opisanie: true,
adress: true,
chasyraboty: {
name: true,
},
_lat: true,
_long: true,
_avtomojka: true,
_dtCopy: true, // ai92
_ai92Copy: true, // ai95
_ai95Copy: true, // z100
_z100Copy: true, // propan
_propanCopy: true, // electricCharge
_zaryadnayaStanci: true, // miniMarket
_miniMarketCop: true, // toilet
_region: {
lat: true,
long: true,
avtomojka: true,
ai92: true,
ai95: true,
z100: true,
propan: true,
zaryadnayastanciya: true,
dt: true,
minimarket: true,
tualet: true,
region: {
name: true,
},
_foto: {
foto: {
url: true,
},
},
@ -73,10 +74,10 @@ export const stationsWithImageRequest = {
};
export const partnersRequest = {
_partners: {
partnyory: {
records: {
_name: true,
_image: {
nazvanie: true,
izobrozhenie: {
url: true,
},
},
@ -84,16 +85,16 @@ export const partnersRequest = {
};
export const jobsRequest = {
_vacancies: {
vakansii: {
records: {
_name: true,
_tags: {
zagolovok: true,
tegi: {
name: true,
},
_type: {
tip: {
name: true,
},
_localtio: {
lokaciya: {
name: true,
},
},
@ -101,23 +102,23 @@ export const jobsRequest = {
};
export const discountsRequest = {
_akcii: {
akcii: {
records: {
_name: true,
_opisanie: true,
_do: true,
_foto: {
url: true,
zagolovok: true,
opisanie: true,
do: true,
foto: {
name: true,
},
},
},
};
export const textsRequest = {
_kontentSajta: {
tekstovyjkontentsajta: {
records: {
_name: true,
_znachenie: true,
znachenie: true,
klyuchneizmenyat: true,
},
},
};
@ -136,35 +137,35 @@ export const mediaRequest = {
};
export const teamRequest = {
_komanda: {
komanda: {
records: {
_foto: {
foto: {
url: true,
},
_zvanie: true,
_name: true,
zvanie: true,
polnoeimya: true,
},
},
};
export const historyRequest = {
_istoriya: {
istoriyakompanii: {
records: {
_name: true,
_god: true,
_opisanie: true,
zagolovok: true,
god: true,
opisanie: true,
},
},
};
export const reviewsRequest = {
_otzyvy: {
otzyvy: {
__args: {
filtersSet: {
conjunction: new EnumType('and'),
filtersSet: [
{
field: new EnumType('_status'),
field: new EnumType('status'),
operator: 'contains',
value: 'Опубликовано',
},
@ -173,21 +174,21 @@ export const reviewsRequest = {
},
records: {
id: true,
_name: true,
_otzyv: true,
_rejting: true,
polnoeimya: true,
otzyv: true,
rejting: true,
},
},
};
export const charityRequest = {
_blagotvoriteln: {
blagotvoritelnyjfond: {
records: {
_name: true,
_opisanie: true,
_data: true,
_lokaciya: true,
_foto: {
zagolovok: true,
opisanie: true,
data: true,
lokaciya: true,
foto: {
url: true,
},
},
@ -195,13 +196,13 @@ export const charityRequest = {
};
export const certificatesRequest = {
_sertifikaty: {
sertifikaty: {
records: {
_name: true,
_opisanie: true,
_dataVydachi: true,
_dejstvitelenDo: true,
_foto: {
nazvanie: true,
opisanie: true,
datavydachi: true,
dejstvitelendo: true,
foto: {
url: true,
},
},
@ -212,7 +213,7 @@ export const createReviewMutation = {
__variables: {
review: 'TableOtzyvyMutationParameters',
},
_otzyvy: {
otzyvy: {
createRecord: {
__args: {
records: [new VariableType('review')],

View File

@ -2,6 +2,7 @@ import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
import { NextRequest } from 'next/server';
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
@ -16,6 +17,10 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
},
});
if (oriyoResponse.status === 401) {
throw new AuthorizationError();
}
return new Response(JSON.stringify(oriyoResponse.data), {
headers: { 'Content-Type': 'application/json' },
});

View File

@ -3,6 +3,7 @@ import { NextRequest } from 'next/server';
import { z } from 'zod';
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
import { getParams } from '@/app/api-utlities/utilities/get-params';
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
@ -34,6 +35,25 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
},
});
if (oriyoResponse.status === 404)
return new Response(
JSON.stringify({
transactions: [],
card_id,
current_page: validatedRequest.page,
limit: validatedRequest.limit,
total_records: 0,
total_pages: 0,
}),
{
headers: { 'Content-Type': 'application/json' },
},
);
if (oriyoResponse.status === 401) {
throw new AuthorizationError();
}
if (oriyoResponse.data.error) throw oriyoResponse.data;
return new Response(JSON.stringify(oriyoResponse.data), {

View File

@ -1,16 +1,15 @@
import { revalidateTag } from 'next/cache';
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';
import { FetchTags } from '@/shared/api/tags';
export async function GET() {
try {
revalidateTag(FetchTags.TAYLOR);
revalidatePath('/', 'layout');
revalidatePath('/', 'page');
return NextResponse.json({ success: true });
} catch (err) {
return NextResponse.json(
{ error: 'Failed to revalidate', detail: err },
{ error: 'Failed to drop cache', detail: err },
{ status: 500 },
);
}

View File

@ -3,6 +3,7 @@ import { NextRequest } from 'next/server';
import { z } from 'zod';
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
import { getParams } from '@/app/api-utlities/utilities/get-params';
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
@ -34,6 +35,24 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
},
});
if (oriyoResponse.status === 404)
return new Response(
JSON.stringify({
transactions: [],
current_page: validatedRequest.page,
limit: validatedRequest.limit,
total_records: 0,
total_pages: 0,
}),
{
headers: { 'Content-Type': 'application/json' },
},
);
if (oriyoResponse.status === 401) {
throw new AuthorizationError();
}
if (oriyoResponse.data.error) throw oriyoResponse.data;
return new Response(JSON.stringify(oriyoResponse.data), {

View File

@ -1,6 +1,7 @@
import { has } from 'lodash';
import { NextRequest, NextResponse } from 'next/server';
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
export const authorizationMiddleware =
(handler: Function, authorizationTokenKey: string) =>
async (req: NextRequest, ...args: any[]) => {
@ -16,7 +17,7 @@ export const authorizationMiddleware =
try {
return await handler(req, requestedToken, ...args);
} catch (error) {
if (has(error, 'code') && error.code === 401) {
if (error instanceof AuthorizationError) {
const response = NextResponse.json(
{ message: 'Authorization session was timed out' },
{ status: 401 },

View File

@ -1,5 +1,4 @@
import { NextRequest } from 'next/server';
import { z } from 'zod';
import { requestTaylor } from '@/app/api-utlities/clients/taylor.client';
import { createReviewMutation } from '@/app/api-utlities/requests/common';
@ -15,9 +14,9 @@ export const POST = async (req: NextRequest) => {
{ mutation: createReviewMutation },
{
review: {
_name: validatedRequest.name,
_otzyv: validatedRequest.reviewMessage,
_rejting: validatedRequest.rating,
polnoeimya: validatedRequest.name,
otzyv: validatedRequest.reviewMessage,
rejting: validatedRequest.rating,
},
},
);

View File

@ -1,6 +1,5 @@
export interface TransactionResponse {
transactions: Transaction[];
card_id: string;
current_page: number;
limit: number;
total_records: number;

View File

@ -37,10 +37,10 @@ export const mainPageApi = taylorAPI.injectEndpoints({
transformResponse: (response: any) => {
return {
partners: presentPartners(response.data._partners),
jobs: presentJobs(response.data._vacancies),
discounts: presentDiscounts(response.data._akcii),
stations: presentStations(response.data._azs),
partners: presentPartners(response.data.partnyory),
jobs: presentJobs(response.data.vakansii),
discounts: presentDiscounts(response.data.akcii),
stations: presentStations(response.data.azs),
};
},
}),
@ -56,10 +56,10 @@ export const mainPageApi = taylorAPI.injectEndpoints({
transformResponse: (response: any) => {
return {
team: presentTeamMembers(response.data._komanda),
history: presentHistoryItems(response.data._istoriya),
stations: presentStations(response.data._azs),
reviews: presentReviews(response.data._otzyvy),
team: presentTeamMembers(response.data.komanda),
history: presentHistoryItems(response.data.istoriyakompanii),
stations: presentStations(response.data.azs),
reviews: presentReviews(response.data.otzyvy),
};
},
}),
@ -75,7 +75,7 @@ export const mainPageApi = taylorAPI.injectEndpoints({
transformResponse: (response: any) => {
return {
charities: presentCharities(response.data._blagotvoriteln),
charities: presentCharities(response.data.blagotvoritelnyjfond),
};
},
}),
@ -91,7 +91,7 @@ export const mainPageApi = taylorAPI.injectEndpoints({
transformResponse: (response: any) => {
return {
certificates: presentCertificates(response.data._sertifikaty),
certificates: presentCertificates(response.data.sertifikaty),
};
},
}),

View File

@ -25,7 +25,7 @@ import { TransactionsTable } from '@/widgets/transactions-table';
export function CorporateDashboard() {
const { t } = useTextController();
const { data, isLoading } = useFetchMyCorporateInfoQuery({});
const { data } = useFetchMyCorporateInfoQuery({});
const [request, setTransactionFetchRequest] = useState<TransactionRequest>({
limit: 10,
@ -159,12 +159,18 @@ export function CorporateDashboard() {
</Card>
</div>
{transactionsResponse && (
<TransactionsTable
data={transactionsResponse}
data={
transactionsResponse || {
limit: 10,
current_page: 1,
total_pages: 0,
total_records: 0,
transactions: [],
}
}
onChange={setTransactionFetchRequest}
/>
)}
</div>
</main>
</div>

View File

@ -148,12 +148,18 @@ export function CustomerDashboard() {
</Card>
</div>
{transactionsResponse && (
<TransactionsTable
data={transactionsResponse}
data={
transactionsResponse || {
limit: 10,
current_page: 1,
total_pages: 0,
total_records: 0,
transactions: [],
}
}
onChange={setTransactionFetchRequest}
/>
)}
</div>
</main>
</div>

View File

@ -6,6 +6,7 @@ const baseQuery = fetchBaseQuery({
baseUrl: process.env.TAYLOR_API_ENDPOINT,
headers: {
Authorization: process.env.TAYLOR_API_TOKEN || '',
schema: 'readable',
},
next: {
tags: [FetchTags.TAYLOR],

View File

@ -18,7 +18,7 @@ export const textControlApi = taylorAPI.injectEndpoints({
}),
transformResponse: (response: any) => {
return presentTexts(response.data._kontentSajta);
return presentTexts(response.data.tekstovyjkontentsajta);
},
}),
}),

View File

@ -2,7 +2,7 @@
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
import { CalendarIcon } from 'lucide-react';
import { CalendarIcon, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import {
@ -46,8 +46,10 @@ export const TransactionsTable = ({
data,
onChange,
}: TransactionsTableProps) => {
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
const [startDate, setStartDate] = useState<Date | undefined>(
new Date(new Date().setMonth(new Date().getMonth() - 1)),
);
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
@ -128,6 +130,9 @@ export const TransactionsTable = ({
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setStartDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
@ -155,16 +160,12 @@ export const TransactionsTable = ({
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setEndDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
</div>
<Button
className='mt-auto bg-red-600 hover:bg-red-700'
onClick={filterTransactions}
>
{t('corporate.transactions.applyButton')}
</Button>
</div>
</div>
@ -204,10 +205,10 @@ export const TransactionsTable = ({
</TableCell>
<TableCell>{transaction.product_name}</TableCell>
<TableCell className='text-right'>
{transaction.price_real}
{transaction.amount}
</TableCell>
<TableCell className='text-right'>
{transaction.amount} {t('corporate.currency')}
{transaction.price_real} {t('corporate.currency')}
</TableCell>
<TableCell className='text-right font-medium'>
{transaction.sum_real} {t('corporate.currency')}