print added

This commit is contained in:
Samandar Turg'unboev
2025-06-25 17:18:25 +05:00
parent 5cad79b822
commit c04c8a74b2
15 changed files with 8954 additions and 8657 deletions

View File

@@ -217,5 +217,6 @@
"enter_product": "请输入产品的追溯ID",
"confirmation": "确认",
"view_packet": "查看包裹数据",
"accepted_number": "已接收"
"accepted_number": "已接收",
"print": "打印"
}

View File

@@ -217,5 +217,6 @@
"enter_product": "Enter your product tracking ID",
"confirmation": "Confirm",
"view_packet": "View package data",
"accepted_number": "Accepted"
"accepted_number": "Accepted",
"print": "Print"
}

View File

@@ -230,5 +230,6 @@
"view_packet": "Просмотреть данные посылки",
"accepted_number": "Принято",
"qr_code": "QR код",
"created_at": "Дата добавления"
"created_at": "Дата добавления",
"print": "Печать"
}

View File

@@ -228,7 +228,7 @@
"product_inspection": "Mahsulotlarni tekshirish",
"enter_product": "Mahsulotning Trassirovka IDni kiriting",
"confirmation": "Tasdiqlash",
"print": "Chop etish",
"qr_code": "QR kod",
"created_at": "Qoshilgan sana"
}

17158
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,7 @@
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.0.0",
"react-select": "^5.8.0",
"react-to-print": "^3.1.0",
"simplebar-react": "^3.2.4",
"swiper": "^11.0.5",
"use-dehydrated-state": "^0.1.0",

BIN
public/instagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

BIN
public/telegram.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -1,33 +1,33 @@
:root {
--app-height: 100%;
--app-height: 100%;
}
html {
box-sizing: border-box;
scroll-behavior: smooth;
font-size: 16px;
box-sizing: border-box;
scroll-behavior: smooth;
font-size: 16px;
}
body {
margin: 0;
padding: 0;
font-weight: 400;
font-size: 1rem;
font-style: normal;
background-color: #fff;
color: #555351;
font-family: "Inter", "Arial", sans-serif !important;
overflow-x: hidden;
margin: 0;
padding: 0;
font-weight: 400;
font-size: 1rem;
font-style: normal;
background-color: #fff;
color: #555351;
font-family: 'Inter', 'Arial', sans-serif !important;
overflow-x: hidden;
}
.dashboard-layout {
font-family: 'SF Pro Display', sans-serif !important;
font-family: 'SF Pro Display', sans-serif !important;
}
*,
*::after,
*::before {
box-sizing: inherit;
box-sizing: inherit;
}
/* *:focus-visible {
@@ -36,47 +36,55 @@ body {
} */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
clip: rect(0 0 0 0);
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
clip: rect(0 0 0 0);
overflow: hidden;
}
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* -------------------------- Number input ni chiziqchalarini ko'rsatmaslik ------------------ */
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
-moz-appearance: textfield;
}
.brd {
border: 1px solid red !important;
}
.printContent {
display: none;
@media print {
@page {
size: 10mm 10mm;
margin: 0;
}
display: block;
}
}

View File

@@ -0,0 +1,118 @@
import InstagramChanel from '@/../public/instagram.png';
import Logo from '@/../public/logo.jpeg';
import TelegramChanel from '@/../public/telegram.jpg';
import { Box, Typography } from '@mui/material';
import Image from 'next/image';
import { forwardRef } from 'react';
interface BoxesPrintProps {
boxData?: {
id: number;
box_name: string;
net_weight: number;
box_weight: number;
box_type: string;
box_size: string;
passportName: string;
status: string;
packetId: number;
partyId: number;
partyName: string;
passportId: string;
client_id: string;
clientName: string;
products_list: Array<{
id: number;
price: number;
cargoId: string;
trekId: string;
name: string;
nameRu: string;
amount: number;
acceptedNumber: number;
weight: number;
}>;
};
}
const BoxesPrint = forwardRef<HTMLDivElement, BoxesPrintProps>(({ boxData }, ref) => {
return (
<Box
ref={ref}
sx={{
display: 'flex',
flexDirection: 'column',
width: '100mm',
height: '100mm',
margin: '0 auto',
fontSize: '9px',
'@media print': {
width: '100mm',
height: '100mm',
},
}}
>
<Box sx={{ border: '1px solid black' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box
sx={{
color: '#fff',
padding: '4px',
display: 'flex',
// flexDirection: 'column',
gap: '12px',
}}
>
<Image alt='logo' src={Logo} width={30} height={30} priority unoptimized />
<Box
sx={{
color: '#fff',
display: 'flex',
flexDirection: 'column',
gap: '4px',
}}
>
<Typography sx={{ color: 'black', fontSize: '10px' }}>CPOST EXPRESS CARGO</Typography>
<Typography sx={{ color: 'black', fontSize: '10px' }}>Reys: {boxData?.partyName}</Typography>
<Typography sx={{ color: 'black', fontSize: '10px' }}>{boxData?.client_id}</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: '4px', marginTop: '5px', marginRight: '5px' }}>
<Image alt='telegram' src={TelegramChanel} width={15} height={15} priority unoptimized />
<Image alt='instagram' src={InstagramChanel} width={15} height={15} priority unoptimized />
<Typography sx={{ color: 'black', fontSize: '8px' }}>TEL: +(998) 90 113 44 77</Typography>
</Box>
</Box>
<Box
sx={{
borderTop: '1px solid black',
textAlign: 'start',
width: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
'@media print': {
pageBreakInside: 'avoid',
},
}}
>
{boxData?.products_list.map((list, index) => (
<Box
sx={{
borderRight: '1px solid black',
textAlign: 'start',
width: 'auto',
padding: '4px',
}}
>
<Typography sx={{ fontSize: '12px' }}>{list.trekId}</Typography>
</Box>
))}
</Box>
</Box>
</Box>
);
});
BoxesPrint.displayName = 'BoxesPrint';
export default BoxesPrint;

View File

@@ -0,0 +1,32 @@
import { forwardRef } from 'react';
import BoxesPrint from './BoxesPrint';
interface BoxesPrintListProps {
boxData: any;
}
const chunkArray = (arr: any[], chunkSize: number): any[][] => {
const result = [];
for (let i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
};
const BoxesPrintList = forwardRef<HTMLDivElement, BoxesPrintListProps>(({ boxData }, ref) => {
const productsChunks = chunkArray(boxData.products_list || [], 36);
return (
<div ref={ref}>
{productsChunks.map((chunk, index) => (
<div key={index} style={{ pageBreakAfter: 'always' }}>
<BoxesPrint boxData={{ ...boxData, products_list: chunk }} />
</div>
))}
</div>
);
});
BoxesPrintList.displayName = 'BoxesPrintList';
export default BoxesPrintList;

View File

@@ -1,16 +1,20 @@
'use client';
import type React from 'react';
import ActionPopMenu from '@/components/common/ActionPopMenu';
import { ColumnData, MyTable } from '@/components/common/MyTable';
import { type ColumnData, MyTable } from '@/components/common/MyTable';
import StatusChangePopup from '@/components/common/StatusChangePopup';
import BaseButton from '@/components/ui-kit/BaseButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import BasePagination from '@/components/ui-kit/BasePagination';
import { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
import { useAuthContext } from '@/context/auth-context';
import { BoxStatus, BoxStatusList, IBox } from '@/data/box/box.model';
import { type BoxStatus, BoxStatusList, type IBox } from '@/data/box/box.model';
import { box_requests } from '@/data/box/box.requests';
import { Product, UpdateProductBodyType } from '@/data/item/item.mode';
import type { Product, UpdateProductBodyType } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests';
import { party_requests } from '@/data/party/party.requests';
import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
import useInput from '@/hooks/useInput';
import { useMyNavigation } from '@/hooks/useMyNavigation';
@@ -19,10 +23,13 @@ import useRequest from '@/hooks/useRequest';
import { file_service } from '@/services/file-service';
import { notifyUnknownError } from '@/services/notification';
import { getStatusColor } from '@/theme/getStatusBoxStyles';
import { Add, Circle, Delete, Download, Edit, FilterList, FilterListOff, RemoveRedEye, Search } from '@mui/icons-material';
import { Add, Circle, Delete, Download, Edit, FilterList, FilterListOff, Print, RemoveRedEye, Search } from '@mui/icons-material';
import { Box, Button, Card, CardContent, Modal, Stack, TextField, Typography } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import AsyncSelect from 'react-select/async';
import { useReactToPrint } from 'react-to-print';
import BoxesPrintList from '../boxes-print/BoxesPrintList';
type Props = {};
@@ -43,7 +50,7 @@ const style = {
const DashboardBoxesPage = (props: Props) => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const [openModal, setOpenModal] = useState(false);
const handleClose = () => setOpen(false);
const t = useMyTranslation();
const navigation = useMyNavigation();
@@ -53,12 +60,96 @@ const DashboardBoxesPage = (props: Props) => {
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
const [boxStatusFilter, setBoxStatusFilter] = useState<BoxStatus | undefined>(undefined);
const [trackId, setTrackId] = useState<string>();
const [partyFilter, setPartyFilter] = useState<{ label: string; value: number } | undefined>(undefined);
const [deleteIds, setDeleteIds] = useState<number[]>([]);
const [downloadIds, setDownloadIds] = useState<number[]>([]);
const [changeStatusIds, setChangeStatusIds] = useState<number[]>([]);
const [boxAmounts, setBoxAmounts] = useState<Record<number, { totalAmount: number; totalAccepted: number }>>({});
// Print uchun state
const [selectedBoxForPrint, setSelectedBoxForPrint] = useState<IBox | null>(null);
const [selectedBoxDetails, setSelectedBoxDetails] = useState<any>(null);
const printRef = useRef<HTMLDivElement>(null);
// Available status options (simulating admin/user permissions)
// Print functionality
const handlePrint = useReactToPrint({
contentRef: printRef,
onAfterPrint: () => {
setSelectedBoxForPrint(null);
setSelectedBoxDetails(null);
},
});
const { data: defaultPartyOptions } = useRequest(() => party_requests.getAll({}), {
enabled: true,
selectData(data) {
return data.data.data.data.map(p => ({ value: p.id, label: p.name }));
},
placeholderData: [],
});
const partyOptions = (inputValue: string) => {
return party_requests.getAll({ partyName: inputValue }).then(res => {
return res.data.data.data.map(p => ({ label: p.name, value: p.id }));
});
};
const onPrintBox = async (boxData: IBox) => {
try {
// Fetch detailed box data
const response = await box_requests.find({ packetId: boxData.id });
const boxOne = response.data.data;
const detailedBoxData = {
id: +boxData.id,
box_name: boxOne.packet.name,
net_weight: +boxOne.packet.brutto,
box_weight: +boxOne.packet.boxWeight,
box_type: boxOne.packet.boxType,
box_size: boxOne.packet.volume,
passportName: boxOne.packet.passportName,
status: boxOne.packet.status,
packetId: boxData.id,
partyId: +boxOne.packet.partyId,
partyName: boxOne.packet.partyName,
passportId: boxOne.client?.passportId,
client_id: boxOne.packet?.cargoId,
clientName: boxOne.client?.passportName,
products_list: boxOne.items.map((item: any) => ({
id: item.id,
price: item.price,
cargoId: item.cargoId,
trekId: item.trekId,
name: item.name,
nameRu: item.nameRu,
amount: +item.amount,
acceptedNumber: item.acceptedNumber,
weight: +item.weight,
})),
};
setSelectedBoxDetails(detailedBoxData);
setTimeout(() => {
handlePrint();
}, 100);
} catch (error) {
console.error('Failed to fetch box details:', error);
}
};
const handleOpen = (data: any) => {
setOpen(true);
};
const handleOpenModal = (data: any) => {
setOpenModal(true);
setSelectedBoxForPrint(data);
};
const boxStatusOptions = useMemo(() => {
const p = ['READY_TO_INVOICE'] as BoxStatus[];
if (isAdmin) {
@@ -72,6 +163,7 @@ const DashboardBoxesPage = (props: Props) => {
box_requests.getAll({
page: page,
cargoId: keyword,
partyId: partyFilter?.value,
status: boxStatusFilter,
direction: 'desc',
sort: 'id',
@@ -191,7 +283,7 @@ const DashboardBoxesPage = (props: Props) => {
getBoxesQuery.refetch();
}, 350);
return () => clearTimeout(timeoutId);
}, [keyword]);
}, [keyword, partyFilter?.value]);
useEffect(() => {
const fetchAmounts = async () => {
@@ -227,7 +319,6 @@ const DashboardBoxesPage = (props: Props) => {
}
}, [list, loading]);
// No, PartyName, PacketName, PartyTozaOg'irlik, CountOfItems, WeightOfItems, CargoID, PassportNameFamily - PacketStatusForInvoice
const columns: ColumnData<IBox>[] = [
{
label: t('No'),
@@ -355,6 +446,26 @@ const DashboardBoxesPage = (props: Props) => {
);
},
},
{
dataKey: 'id',
label: t('print'),
width: 120,
renderHeaderCell(rowIndex) {
return (
<Stack direction={'row'} alignItems={'center'}>
<span>{t('print')}</span>
</Stack>
);
},
renderCell(data) {
const total = boxAmounts[data.id];
return (
<Button onClick={() => onPrintBox(data)} disabled={total?.totalAccepted !== total?.totalAmount}>
<Print className='h-3 w-3 mr-1' />
</Button>
);
},
},
{
label: '',
width: 100,
@@ -405,6 +516,7 @@ const DashboardBoxesPage = (props: Props) => {
},
},
];
const [items, setItems] = useState<Product>();
const [loaer, setLoading] = useState(false);
const {
@@ -431,7 +543,7 @@ const DashboardBoxesPage = (props: Props) => {
const updateBody: UpdateProductBodyType = {
itemId: item.id,
acceptedNumber,
acceptedNumber: item.amount,
amount: item.amount,
name: item.name,
nameRu: item.nameRu,
@@ -441,11 +553,11 @@ const DashboardBoxesPage = (props: Props) => {
await item_requests.update(updateBody);
// Ma'lumotni yangilab olamiz
getListQuery.refetch();
getBoxesQuery.refetch();
setValues(prev => ({ ...prev, [item.trekId]: '' }));
setTrackId('');
} catch (error) {
notifyUnknownError(error);
} finally {
@@ -472,12 +584,13 @@ const DashboardBoxesPage = (props: Props) => {
id='outlined-basic'
label={t('track_id')}
variant='outlined'
value={trackId}
onChange={e => setTrackId(e.target.value)}
/>
{trackId && trackId.length > 0 && (
<>
{getListQuery.loading ? (
<Typography sx={{ mt: 2 }}>{t('loading')}...</Typography> // yoki <CircularProgress />
<Typography sx={{ mt: 2 }}>{t('loading')}...</Typography>
) : getListQuery.data?.data && getListQuery.data?.data.length > 0 ? (
getListQuery.data?.data.map(e => (
<Box key={e.id} sx={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
@@ -491,15 +604,6 @@ const DashboardBoxesPage = (props: Props) => {
<Typography sx={{ fontSize: 14 }}>Paket nomi: {e?.packetName}</Typography>
</CardContent>
</Card>
<TextField
id={`amount-${e.trekId}`}
label='Mahsulot soni'
type='number'
sx={{ width: '100%' }}
value={values[e.trekId] ?? ''}
onChange={change => handleAmountChange(change, e.amount, e.trekId)}
inputProps={{ min: 1, max: e.amount }}
/>
<Button
sx={{ mt: '10px' }}
onClick={() => {
@@ -552,7 +656,20 @@ const DashboardBoxesPage = (props: Props) => {
setKeyword(e.target.value);
}}
/>
<AsyncSelect
isClearable
value={partyFilter}
onChange={(newValue: any) => {
setPartyFilter(newValue);
setPage(1);
}}
styles={selectDefaultStyles}
noOptionsMessage={() => t('not_found')}
loadingMessage={() => t('loading')}
defaultOptions={defaultPartyOptions!}
loadOptions={partyOptions}
placeholder={t('filter_party_name')}
/>
<BaseButton colorVariant='gray' startIcon={<FilterListOff />} size='small' onClick={resetFilter}>
{t('reset_filter')}
</BaseButton>
@@ -565,6 +682,13 @@ const DashboardBoxesPage = (props: Props) => {
<BasePagination page={page} pageSize={pageSize} totalCount={totalElements} onChange={handleChange} />
</Stack>
</Box>
{/* Print komponenti - sahifaning oxirida yashirin */}
{selectedBoxDetails && (
<div style={{ display: 'none' }}>
<BoxesPrintList ref={printRef} boxData={selectedBoxDetails} />
</div>
)}
</Box>
);
};

View File

@@ -44,7 +44,7 @@ const DashboardClientsPage = (props: Props) => {
customer_requests.getAll({
page: page,
clientName: name,
autoCargoId: keyword,
sort: keyword,
}),
{
selectData(data) {

View File

@@ -1,34 +1,30 @@
'use client';
import AsyncSelect from 'react-select/async';
import ActionPopMenu from '@/components/common/ActionPopMenu';
import { MyTable, ColumnData } from '@/components/common/MyTable';
import StatusChangePopup from '@/components/common/StatusChangePopup';
import { ColumnData, MyTable } from '@/components/common/MyTable';
import BaseButton from '@/components/ui-kit/BaseButton';
import BaseIconButton from '@/components/ui-kit/BaseIconButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import BasePagination from '@/components/ui-kit/BasePagination';
import { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
import { useAuthContext } from '@/context/auth-context';
import { BoxStatus, BoxStatusList } from '@/data/box/box.model';
import { box_requests } from '@/data/box/box.requests';
import { Product } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests';
import { Party, PartyStatus, PartyStatusList } from '@/data/party/party.model';
import { party_requests } from '@/data/party/party.requests';
import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
import { UserRoleEnum } from '@/data/user/user.model';
import { DEFAULT_PAGE_SIZE } from '@/helpers/constants';
import useInput from '@/hooks/useInput';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import useRequest from '@/hooks/useRequest';
import AddPhotosModal from '@/routes/private/items/components/AddPhotosModal';
import EditItemModal from '@/routes/private/items/components/EditItemModal';
import { notifyUnknownError } from '@/services/notification';
import { getBoxStatusStyles, getStatusColor } from '@/theme/getStatusBoxStyles';
import { Add, AddCircleOutline, Check, Circle, Delete, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material';
import { Box, Stack, SvgIcon, Tooltip, Typography } from '@mui/material';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
import { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
import { box_requests } from '@/data/box/box.requests';
import { useAuthContext } from '@/context/auth-context';
import { UserRoleEnum } from '@/data/user/user.model';
import AddPhotosModal from '@/routes/private/items/components/AddPhotosModal';
import { Add, Check, Circle, Delete, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material';
import { Box, Stack, Typography } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import AsyncSelect from 'react-select/async';
type Props = {};

View File

@@ -3405,6 +3405,11 @@ react-select@*, react-select@^5.8.0:
react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.2.0"
react-to-print@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/react-to-print/-/react-to-print-3.1.0.tgz"
integrity sha512-hiJZVmJtaRm9EHoUTG2bordyeRxVSGy9oFVV7fSvzOWwctPp6jbz2R6NFkaokaTYBxC7wTM/fMV5eCXsNpEwsA==
react-transition-group@^4.3.0, react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"
@@ -3415,7 +3420,7 @@ react-transition-group@^4.3.0, react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
"react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", react@^18, "react@^18 || ^19", react@^18.2.0, "react@>= 16.0.0", "react@>= 16.8.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", "react@>= 17.0.2", react@>=16, react@>=16.6.0, react@>=16.8, react@>=16.8.0:
"react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ~19", "react@^17.0.0 || ^18.0.0 || ^19.0.0", react@^18, "react@^18 || ^19", react@^18.2.0, "react@>= 16.0.0", "react@>= 16.8.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", "react@>= 17.0.2", react@>=16, react@>=16.6.0, react@>=16.8, react@>=16.8.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==