Merge branch 'main' into production
This commit is contained in:
15
.env
15
.env
@ -8,24 +8,23 @@ NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
||||
|
||||
NODE_ENV=development
|
||||
# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma
|
||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
||||
# DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
DATABASE=mysql://db:db@192.168.0.10:3306/cart
|
||||
# NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
||||
ADMIN_PASSWORD=123456
|
||||
|
||||
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||
# callback https://sofia.mwitnessing.com/api/auth/callback/google
|
||||
GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
||||
GOOGLE_SECRET=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
|
||||
|
||||
|
||||
# //https://sofia.mwitnessing.com/api/auth/callback/microsoft
|
||||
# https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
|
||||
# owner: dobromirpopovgateway.onmicrosoft.com dobromir.popov@gateway.one (personal) Doby Popov P One
|
||||
# callback https://sofia.mwhitnessing.com/api/auth/callback/azure-ad
|
||||
|
||||
AZURE_AD_CLIENT_ID=9e13bedd-1f9d-4c23-910e-a806aba308b6 # Application (client) ID
|
||||
|
||||
AZURE_AD_CLIENT_ID=9e13bedd-1f9d-4c23-910e-a806aba308b6 # Application (client) ID
|
||||
AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret value
|
||||
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
||||
|
||||
@ -55,8 +54,6 @@ FACEBOOK_SECRET=
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert
|
||||
ENV_ENV=.env.development
|
||||
PROTOCOL=https
|
||||
@ -8,7 +8,6 @@ NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
||||
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
|
||||
|
||||
EMAIL_SENDER='"ССОМ [ТЕСТ] " <mwitnessing@gmail.com>'
|
||||
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
# MAILTRAP_HOST=sandbox.smtp.mailtrap.io
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,3 +37,4 @@ public/content/output/shifts 2024.1.json
|
||||
!public/content/uploads/*
|
||||
.aider*
|
||||
/shift_generate_log_*.txt
|
||||
.env
|
||||
|
@ -16,7 +16,18 @@ services:
|
||||
- GIT_USERNAME=deploy
|
||||
- GIT_PASSWORD=L3Kr2R438u4F7
|
||||
- ADMIN_PASSWORD=kolichkisofia2024
|
||||
command: sh -c " cd /app && npm install && npx next build && npm run start-env; tail -f /dev/null"
|
||||
command: >
|
||||
sh -c "
|
||||
cd /app &&
|
||||
rm -rf node_modules package-lock.json &&
|
||||
npm install --no-package-lock &&
|
||||
npm uninstall @prisma/client prisma &&
|
||||
npm install @prisma/client@5.22.0 prisma@5.22.0 --save-exact &&
|
||||
npx next build &&
|
||||
npx prisma migrate deploy &&
|
||||
npx prisma generate &&
|
||||
npm run start-env;
|
||||
tail -f /dev/null"
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: always
|
||||
|
@ -3,7 +3,7 @@
|
||||
if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then
|
||||
# Install necessary packages
|
||||
apk add git nano rsync
|
||||
echo "Updating code from git.d-popov.com...(as '$GIT_USERNAME')" > /app/logs/deploy.txt
|
||||
echo "Updating code from git.d-popov.com...(as '$GIT_USERNAME'). Branch: ${GIT_BRANCH:main}" > /app/logs/deploy.txt
|
||||
|
||||
# Create a temporary directory for the new clone
|
||||
rm -rf /tmp/clone
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
ports:
|
||||
- 8083:8080
|
||||
|
||||
# THE APP
|
||||
# THE APP
|
||||
nextjs-app:
|
||||
image: sachinvashist/nextjs-docker
|
||||
ports:
|
||||
@ -61,5 +61,3 @@ services:
|
||||
stdin_open: true
|
||||
# depends_on:
|
||||
# - mariadb
|
||||
|
||||
|
@ -148,6 +148,12 @@ npx prisma migrate resolve --applied 20240201214719_assignment_add_repeat_freque
|
||||
|
||||
npx prisma migrate dev --schema "mysql://cart:cart2023@192.168.0.10:3306/cart_dev" # -- does not work
|
||||
|
||||
# reset to version:
|
||||
npm uninstall @prisma/client prisma
|
||||
npm install @prisma/client@5.22.0 prisma@5.22.0
|
||||
# check current:
|
||||
npm why prisma
|
||||
|
||||
|
||||
## ---------------------- import database --------------------------------- ##
|
||||
gunzip < /prisma/backups/jwpwsofia-20240430-bak.gz | mysql -u mysql_username -p database_name
|
||||
|
@ -9,6 +9,7 @@ import FileUploadWithPreview from 'components/FileUploadWithPreview ';
|
||||
|
||||
import ProtectedRoute, { serverSideAuth } from "../../components/protectedRoute";
|
||||
import { UserRole } from "@prisma/client";
|
||||
import { Input } from '@mui/base';
|
||||
|
||||
const common = require('src/helpers/common');
|
||||
|
||||
@ -37,8 +38,9 @@ export default function LocationForm() {
|
||||
useEffect(() => {
|
||||
const fetchUploadedImages = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get('/uploaded-images');
|
||||
setUploadedImages(response.data.imageUrls);
|
||||
// ToDo: we don't have this endpoint yet and we don't use the uploaded images collection
|
||||
// const response = await axiosInstance.get('/uploaded-images');
|
||||
// setUploadedImages(response.data.imageUrls);
|
||||
} catch (error) {
|
||||
console.error('Error fetching uploaded images:', error);
|
||||
}
|
||||
@ -67,6 +69,7 @@ export default function LocationForm() {
|
||||
address: "",
|
||||
isActive: true,
|
||||
});
|
||||
const [isRawHtml, setIsRawHtml] = useState(false);
|
||||
|
||||
// const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
@ -178,6 +181,13 @@ export default function LocationForm() {
|
||||
<label className="label" htmlFor="isActive">Активна</label>
|
||||
</div>
|
||||
</div>
|
||||
{/* is on main menu */}
|
||||
<div className="mb-4">
|
||||
<div className="form-check">
|
||||
<input className="checkbox form-input" type="checkbox" id="isOnMainMenu" name="isOnMainMenu" onChange={handleChange} checked={location.isOnMainMenu} autoComplete="off" />
|
||||
<label className="label" htmlFor="isOnMainMenu">Покажи в главното меню</label>
|
||||
</div>
|
||||
</div>
|
||||
{/* backupLocation */}
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="backupLocation">При дъжд и лошо време</label>
|
||||
@ -238,12 +248,50 @@ export default function LocationForm() {
|
||||
|
||||
<label className="label" htmlFor="content">Content</label>
|
||||
|
||||
<TextEditor
|
||||
ref={quillRef}
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder="Описание на локацията. Снимки"
|
||||
prefix={`location-${router.query.id}`} />
|
||||
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
||||
<label className="label" htmlFor="backupLocation">Admin Edit Location HTML</label>
|
||||
<div className="field mb-4">
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isRawHtml}
|
||||
onChange={(e) => {
|
||||
setIsRawHtml(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="slider round"></span>
|
||||
<span className="ml-2">Edit Raw HTML</span>
|
||||
|
||||
</label>
|
||||
{isRawHtml && <>
|
||||
<label className="label" htmlFor="backupLocation">Admin Edit Location HTML</label>
|
||||
<textarea
|
||||
className="w-full min-h-[200px] p-3 border rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200 font-mono resize-y"
|
||||
placeholder="HTML Content"
|
||||
id="contentHTML_Raw"
|
||||
name="contentHTML_Raw"
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
value={content}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
{!isRawHtml && <>
|
||||
<TextEditor
|
||||
ref={quillRef}
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder="Описание на локацията. Снимки"
|
||||
prefix={`location-${router.query.id}`} />
|
||||
|
||||
</>}
|
||||
|
||||
|
||||
</>)}
|
||||
</div>
|
||||
<div className="panel-actions pt-12">
|
||||
@ -270,3 +318,4 @@ export default function LocationForm() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,6 +95,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) {
|
||||
delete item.shift;
|
||||
} else {
|
||||
item.shift = { connect: { id: parseInt(item.shiftId) } };
|
||||
// item.shift = { connect: { id: item.shiftId.toString() } };
|
||||
}
|
||||
delete item.shiftId;
|
||||
item.date = new Date(item.date);
|
||||
|
@ -112,20 +112,35 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
||||
useEffect(() => {
|
||||
const fetchLocations = async () => {
|
||||
try {
|
||||
if (session) { // Don't fetch locations if the user is not authenticated
|
||||
// if (session)
|
||||
{ // Don't fetch locations if the user is not authenticated
|
||||
|
||||
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
|
||||
const locationsData = response.data
|
||||
.filter(location => location.isActive === true)
|
||||
const subMenuLocations = response.data
|
||||
.filter(location => location.isActive === true && location.isOnMainMenu === false)
|
||||
.map(location => ({
|
||||
text: location.name,
|
||||
url: `/cart/locations/${location.id}`,
|
||||
isOnMainMenu: location.isOnMainMenu,
|
||||
}));
|
||||
const mainMenuLocations = response.data.filter(l => l.isOnMainMenu === true)
|
||||
.map(location => ({
|
||||
key: location.name,
|
||||
text: t('location.' + location.name + '.menu') || location.name,
|
||||
url: location.url,
|
||||
}));
|
||||
|
||||
console.log("locationsData: ", response.data);
|
||||
console.log("subMenuLocations: ", subMenuLocations);
|
||||
console.log("mainMenuLocations: ", mainMenuLocations);
|
||||
// Find the "Locations" menu item and populate its children with locationsData
|
||||
const menuIndex = sidemenu.findIndex(item => item.id === "locations");
|
||||
if (menuIndex !== -1) {
|
||||
sidemenu[menuIndex].children = locationsData;
|
||||
sidemenu[menuIndex].children = subMenuLocations;
|
||||
}
|
||||
// insert main menu items after the locations (sidemenu[menuIndex+1])
|
||||
|
||||
sidemenu.splice(menuIndex + 1, 0, ...mainMenuLocations);
|
||||
//setLocations(locationsData); // Optional, if you need to use locations elsewhere
|
||||
}
|
||||
} catch (error) {
|
||||
@ -164,7 +179,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
||||
<div className="mt-auto">
|
||||
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
|
||||
<FooterSection />
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]} deniedMessage="">
|
||||
<LanguageSwitcher />
|
||||
</ProtectedRoute >
|
||||
</div>
|
||||
|
@ -34,6 +34,12 @@ const sidemenu = [
|
||||
collapsable: true,
|
||||
url: "/cart/locations",
|
||||
},
|
||||
{
|
||||
id: "warehouse",
|
||||
text: "Склад",
|
||||
url: "/cart/locations/warehouse",
|
||||
svgData: "M7 21V11.6C7 11.0399 7 10.7599 7.10899 10.546C7.20487 10.3578 7.35785 10.2048 7.54601 10.109C7.75992 9.99996 8.03995 9.99996 8.6 9.99996H15.4C15.9601 9.99996 16.2401 9.99996 16.454 10.109C16.6422 10.2048 16.7951 10.3578 16.891 10.546C17 10.7599 17 11.0399 17 11.6V21M10 14H14M10 18H14M3 10.4881V19.4C3 19.96 3 20.24 3.10899 20.454C3.20487 20.6421 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7951 20.7951 20.6421 20.891 20.454C21 20.24 21 19.96 21 19.4V10.4881C21 9.41436 21 8.87747 20.8368 8.40316C20.6925 7.98371 20.457 7.60148 20.1472 7.28399C19.797 6.92498 19.3174 6.68357 18.3583 6.20075L14.1583 4.08645C13.3671 3.68819 12.9716 3.48905 12.5564 3.41069C12.1887 3.34129 11.8113 3.34129 11.4436 3.41069C11.0284 3.48905 10.6329 3.68818 9.84171 4.08645L5.64171 6.20075C4.6826 6.68357 4.20304 6.92498 3.85275 7.28399C3.54298 7.60148 3.30746 7.98371 3.16317 8.40316C3 8.87747 3 9.41437 3 10.4881Z"
|
||||
},
|
||||
{
|
||||
id: "cart-report",
|
||||
text: "Отчет",
|
||||
|
@ -31,13 +31,19 @@
|
||||
"cart-reports": "Отчети",
|
||||
"statistics": "Статистика",
|
||||
"coverMeLogs": "Замествания",
|
||||
"translations": "Преводи"
|
||||
"translations": "Преводи",
|
||||
"warehouse": "Склад Сердика"
|
||||
},
|
||||
"content": {
|
||||
"location": {
|
||||
"warehouse": {
|
||||
"description": "- снимки как да се поставят количките\n- снимка с код за катинар\nвсеки може да всима/връща\n- преди да влизаме трябва да почистим краката.\n- зареждаме/почистваме извън\n- влизане/озлизане няма код\n- влизане/излизане има код",
|
||||
"title": "СНЛАД"
|
||||
"title": "Склад Сердика",
|
||||
"description_1": "**1. Катинар.** Шифъра на катинара е: 1914.\n",
|
||||
"description_2": "**2. Място за съхранение.** (Подлеза на метростанция Сердика в близост до църквата \"Света Петка Самарджийска\")\n\nЛинк: [LockStars Lockers](https://www.google.com/maps/place/LockStars+Lockers+-+luggage+storage+in+Sofia/@42.697779,23.3218397,20.15z/data=!4m6!3m5!1s0x40aa85dd76aed8a9:0xa0b10cc277013ee9!8m2!3d42.6978913!4d23.3223441!16s%2Fg%2F11vlhc26h5?authuser=0&entry=tts&g_ep)\n\n\n",
|
||||
"description_3": "**3. Позицията на щандовете**, начин на съхранение, както и мястото, може да видите в прикачените снимки.\n\n ***Вратата на гардероба е винаги отворена. Няма нужда да натискаме бутона за отваряне**\n\n",
|
||||
"description_4": "**4. Зареждането и почистване на щандовете трябва да става извън мястото за съхранение. ** Също когато прибираме щандовете е необходимо да са почистени и в добър външен вид с калъф на тях(гумите/колелата да са почистени от кал за да не оставяме следи по пода)\n\n**Важно:** посоката на плакатите и на двата щанда трябва да е към стената! Целта е да не се виждат, тъй като **в помещението са забранени рекламни и други материали извън дейността на фирмата.**",
|
||||
"description_5": "**5. Литература.** Ще има метална кутия за съхранение на литература в непосредствена близост над щандовете на единия от сейфовете.\n\nМоля бъдете особено внимателни когато сваляте и качвате кутията за да не се нараните.",
|
||||
"description_6": "**6. Престой.** Хубаво ще е да не се задържаме прекалено дълго в помещението, повече от необходимо да вземем количките и нужнаta литература."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,12 @@
|
||||
"statistics": "Статистика",
|
||||
"coverMeLogs": "Замествания",
|
||||
"translations": "Преводи"
|
||||
},
|
||||
"content": {
|
||||
"location": {
|
||||
"warehouse": {
|
||||
"description": "СКЛАД СЕРДИКА\n1. Катинар. Шифъра на катинара е: 1914. \n2. Място за съхранение (Подлеза на метростанция Сердика в близост до църквата \"Света Петка Самарджийска\") \nЛинк: https: //maps.app.goo.gl/FfzZww2EfxeP2y9S7\n3. Позицията на щандовете, начин на съхранение, както и мястото, може да видите в прикачените снимки. \n* Вратата на гардероба е винаги отворена. Няма нужда да натискаме бутона за отваряне\n4. Зареждането и почистване на щандовете трябва да става извън мястото за съхранение. Също когато прибираме щандовете е необходимо да са почистени и в добър външен вид с калъф на тях(гумите/колелата да са почистени от кал за да не оставяме следи по пода)\nВажно: посоката на плакатите и на двата щанда трябва да е към стената! Целта е да не се виждат, тъй като в помещението са забранени рекламни и други материали извън дейността на фирмата.\n5. Литература. Ще има метална кутия за съхранение на литература в непосредствена близост над щандовете на единия от сейфовете.\nМоля бъдете особено внимателни когато сваляте и качвате кутията за да не се нараните. \n6. Престой. Хубаво ще е да не се задържаме прекалено дълго в помещението, повече от необходимо да вземем количките и нужнаta литература. "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,8 +36,13 @@
|
||||
"content": {
|
||||
"location": {
|
||||
"warehouse": {
|
||||
"description": "- снимки как да се поставят количките\n- снимка с код за катинар\nвсеки може да всима/връща\n- преди да влизаме трябва да почистим краката.\n- зареждаме/почистваме извън\n- влизане/озлизане няма код\n- влизане/излизане има код",
|
||||
"title": "СНЛАД"
|
||||
"title": "Serdika Storage",
|
||||
"description_1": "**1. Padlock.** The padlock code is: 1914.",
|
||||
"description_2": "**2. Storage Location.** (Serdika Metro Station underpass near the 'St. Petka Samardzhiyska' church)\n\nLink: [LockStars Lockers]",
|
||||
"description_3": "**3. Stand Positions**, storage method, and location can be seen in the attached photos.\n\n***The locker door is always open. No need to press the opening button***",
|
||||
"description_4": "**4. Loading and cleaning of stands must be done outside the storage area.** Also, when storing the stands, they need to be clean and in good external condition with covers on them (tires/wheels should be cleaned of mud to avoid leaving traces on the floor)\n\n**Important:** the direction of posters on both stands should face the wall! The goal is to keep them hidden, as **advertising and other materials unrelated to the company's activities are prohibited in the premises.**",
|
||||
"description_5": "**5. Literature.** There will be a metal box for storing literature in close proximity above the stands on one of the safes.\n\nPlease be especially careful when lowering and raising the box to avoid injury.",
|
||||
"description_6": "**6. Stay Duration.** It's good not to stay in the premises longer than necessary to take the carts and required literature."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,13 @@
|
||||
"content": {
|
||||
"location": {
|
||||
"warehouse": {
|
||||
"description": "- снимки как да се поставят количките\n- снимка с код за катинар\nвсеки може да всима/връща\n- преди да влизаме трябва да почистим краката.\n- зареждаме/почистваме извън\n- влизане/озлизане няма код\n- влизане/излизане има код",
|
||||
"title": "СНЛАД"
|
||||
"title": "Склад Сердика",
|
||||
"description_1": "**1. Замок.** Код замка: 1914.",
|
||||
"description_2": "**2. Место хранения.** (Подземный переход станции метро Сердика рядом с церковью 'Св. Петки Самарджийской')\n\nСсылка: [LockStars Lockers]",
|
||||
"description_3": "**3. Расположение стендов**, способ хранения и место можно увидеть на прикрепленных фотографиях.\n\n***Дверь шкафа всегда открыта. Нет необходимости нажимать кнопку открытия***",
|
||||
"description_4": "**4. Загрузка и очистка стендов должны производиться вне места хранения.** Также при размещении стендов они должны быть чистыми и в хорошем внешнем состоянии с чехлами (шины/колёса должны быть очищены от грязи, чтобы не оставлять следов на полу)\n\n**Важно:** направление плакатов на обоих стендах должно быть к стене! Цель - сделать их невидимыми, так как **в помещении запрещены рекламные и другие материалы, не связанные с деятельностью компании.**",
|
||||
"description_5": "**5. Литература.** Будет металлический ящик для хранения литературы в непосредственной близости над стендами на одном из сейфов.\n\nПожалуйста, будьте особенно осторожны при опускании и поднятии ящика, чтобы избежать травм.",
|
||||
"description_6": "**6. Продолжительность пребывания.** Желательно не задерживаться в помещении дольше, чем необходимо для того, чтобы взять тележки и нужную литературу."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6900
package-lock.json
generated
6900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -26,6 +26,10 @@
|
||||
"@helpers": "./src/helpers"
|
||||
},
|
||||
"dependencies": {
|
||||
"prisma": "^5.22.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"prisma-json-schema-generator": "^5.1.1",
|
||||
"@premieroctet/next-crud": "^3.0.0",
|
||||
"@auth/prisma-adapter": "^1.4.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
@ -33,8 +37,6 @@
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@mui/x-date-pickers": "^6.19.4",
|
||||
"@premieroctet/next-crud": "^3.0.0",
|
||||
"@prisma/client": "^5.19.1",
|
||||
"@react-pdf/renderer": "^3.3.8",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/multer": "^1.4.11",
|
||||
@ -96,10 +98,13 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-toastify": "^10.0.4",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tw-elements": "^1.1.0",
|
||||
@ -118,7 +123,6 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"depcheck": "^1.4.7",
|
||||
"eslint-plugin-no-unsanitized": "^4.1.0",
|
||||
"prisma": "^5.19.1"
|
||||
"eslint-plugin-no-unsanitized": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
3
pages/404.js
Normal file
3
pages/404.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Custom404() {
|
||||
return <h1>404 - Страницата не е намерена</h1>
|
||||
}
|
@ -3,14 +3,20 @@ import { Prisma } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
// import { getToken } from "next-auth/jwt";
|
||||
// import { getSession } from "next-auth/client";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
const common = require("../../../src/helpers/common");
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { decode } from 'next-auth/jwt';
|
||||
const logger = require('../../../src/logger');
|
||||
|
||||
// import { getToken } from "next-auth/jwt";
|
||||
interface SessionUser {
|
||||
email?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
user?: SessionUser;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const prismaClient = common.getPrismaClient();
|
||||
@ -19,27 +25,35 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
adapter: new PrismaAdapter({ prismaClient }),
|
||||
models: {
|
||||
[Prisma.ModelName.CartEvent]: { name: "cartevents" },
|
||||
[Prisma.ModelName.Publisher]: { name: "publishers" },
|
||||
[Prisma.ModelName.Availability]: { name: "availabilities" },
|
||||
[Prisma.ModelName.Location]: { name: "locations" },
|
||||
[Prisma.ModelName.Shift]: { name: "shifts" },
|
||||
[Prisma.ModelName.Assignment]: { name: "assignments" },
|
||||
[Prisma.ModelName.Report]: { name: "reports" },
|
||||
[Prisma.ModelName.Message]: { name: "messages" },
|
||||
[Prisma.ModelName.Survey]: { name: "surveys" },
|
||||
[Prisma.ModelName.EventLog]: { name: "eventlogs" },
|
||||
},
|
||||
});
|
||||
|
||||
//1: check session
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
//console.log("Session:", session); // Log the session
|
||||
const session = (await getServerSession(req, res, authOptions)) as Session | null;
|
||||
const authHeader = req.headers.authorization || '';
|
||||
//console.log('authHeader', authHeader);
|
||||
if (session) {
|
||||
|
||||
if (session && req.query.nextcrud) {
|
||||
//get target table
|
||||
const targetTable = req.query.nextcrud[0];
|
||||
//get target action
|
||||
if (req.method === 'DELETE') {
|
||||
switch (targetTable) {
|
||||
// case 'publishers':
|
||||
// case 'availabilities':
|
||||
default:
|
||||
const targetId = req.query.nextcrud[1];
|
||||
logger.info('[nextCrud] ' + targetTable + ': ' + targetId + ' DELETED by ' + session.user.email);
|
||||
logger.info('[nextCrud] ' + targetTable + ': ' + targetId + ' DELETED by ' + session.user?.email);
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('[nextCrud]: request for ' + targetTable + '. params:', req.query);
|
||||
return nextCrudHandler(req, res);
|
||||
}
|
||||
else {
|
||||
@ -49,21 +63,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
//2: check jwt
|
||||
const secret = process.env.NEXTAUTH_SECRET;
|
||||
const bearerHeader = req.headers['authorization'];
|
||||
if (bearerHeader) {
|
||||
if (bearerHeader && secret) {
|
||||
const token = bearerHeader.split(' ')[1]; // Assuming "Bearer <token>"
|
||||
try {
|
||||
const decoded = await decode({
|
||||
token: token,
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
});
|
||||
//console.log('Decoded JWT:');
|
||||
} catch (err) {
|
||||
console.error('[nextCrud]: Error decoding token:', err);
|
||||
}
|
||||
// try {
|
||||
// const decodedToken = await getToken({ req, secret });
|
||||
// if (decodedToken) {
|
||||
// return nextCrudHandler(req, res);
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error('[nextCrud]: Error decoding token:', err);
|
||||
// }
|
||||
try {
|
||||
const verified = jwt.verify(token, secret);
|
||||
//console.log('Verified JWT:');
|
||||
|
||||
return nextCrudHandler(req, res);
|
||||
} catch (err) {
|
||||
console.error('[nextCrud]: Invalid token:', err);
|
||||
@ -76,7 +87,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return nextCrudHandler(req, res);
|
||||
}
|
||||
|
||||
|
||||
return res.status(401).json({ message: '[nextCrud]: Unauthorized' });
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../auth/[...nextauth]";
|
||||
import e from 'express';
|
||||
|
||||
|
||||
const common = require('../../../../src/helpers/common');
|
||||
@ -71,10 +72,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (req.method === 'POST' && req.headers['content-type']?.includes('application/json')) {
|
||||
// Handle POST request
|
||||
queryOptions = req.body;
|
||||
} else {
|
||||
} else if (req.method === 'GET') {
|
||||
// Handle GET request
|
||||
queryOptions = parseQueryParams(req.query);
|
||||
}
|
||||
else if (req.method === 'DELETE') {
|
||||
// Handle DELETE request
|
||||
queryOptions = req.body;
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
if (!modelArray || modelArray.length === 0) {
|
||||
@ -84,6 +90,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (!prisma[modelName]) {
|
||||
throw new Error(`Model ${modelName} not found in Prisma client.`);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
// Create a new record
|
||||
const result = await prisma[modelName].create({ data: queryOptions });
|
||||
res.status(201).json(result);
|
||||
return;
|
||||
} else if (req.method === 'DELETE') {
|
||||
// Delete a record
|
||||
const result = await prisma[modelName].delete({ where: queryOptions });
|
||||
res.status(200).json(result);
|
||||
return;
|
||||
}
|
||||
// Fetch records
|
||||
|
||||
const result = await prisma[modelName].findMany(queryOptions);
|
||||
if (req.query.format === 'sql') {
|
||||
// Generate SQL if requested via query parameter
|
||||
|
@ -12,7 +12,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { all } from "axios";
|
||||
import { logger } from "src/helpers/common";
|
||||
import { ExportPublishersToExcel } from "src/helpers/excel";
|
||||
import { generatePublishersExcel } from "src/helpers/excel";
|
||||
|
||||
/**
|
||||
*
|
||||
@ -435,9 +435,16 @@ export default async function handler(req, res) {
|
||||
res.status(200).json(await dataHelper.getAllPublishersWithStatisticsMonth(day, noEndDate));
|
||||
case "exportPublishersExcel":
|
||||
try {
|
||||
await ExportPublishersToExcel(req, res);
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().split('T')[0]; // Gets YYYY-MM-DD format
|
||||
|
||||
const excelBuffer = await generatePublishersExcel();
|
||||
res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
res.setHeader("Content-Disposition", "attachment; filename=" + encodeURI(`Publishers_${dateStr}.xlsx`));
|
||||
res.send(excelBuffer);
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify(error));
|
||||
res.status(500).json({ error: "Failed to generate Excel file" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -53,6 +53,12 @@ const Notification = async (req, res) => {
|
||||
select: { pushSubscription: true }
|
||||
});
|
||||
|
||||
if (!publisher) {
|
||||
res.statusCode = 404
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
let subscriptions = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription : (publisher.pushSubscription ? [publisher.pushSubscription] : []);
|
||||
const index = subscriptions.findIndex(sub => sub.endpoint === subscription.endpoint);
|
||||
|
||||
|
@ -6,6 +6,9 @@ import { GetServerSideProps } from 'next';
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { useTranslations, createTranslator } from 'next-intl';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
// import { getTranslations } from 'next-intl/server';
|
||||
|
||||
const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
|
||||
@ -54,15 +57,17 @@ const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
|
||||
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'mainLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
|
||||
onClick={() => handleTabChange('mainLocation')}
|
||||
>
|
||||
{location.name}
|
||||
{location.title || location.name}
|
||||
</button>
|
||||
{/* Backup Location Tab */}
|
||||
<button
|
||||
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'backupLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
|
||||
onClick={() => handleTabChange('backupLocation')}
|
||||
>
|
||||
При лошо време: <strong>{location.backupLocationName}</strong>
|
||||
</button>
|
||||
{location.backupLocationId !== null && (
|
||||
<button
|
||||
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'backupLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
|
||||
onClick={() => handleTabChange('backupLocation')}
|
||||
>
|
||||
При лошо време: <strong>{location.backupLocationName}</strong>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
@ -85,8 +90,27 @@ const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
|
||||
{/* Tab Content */}
|
||||
{(location.content || location.backupLocationContent) && (
|
||||
<div className="tab-content mt-4">
|
||||
|
||||
{activeTab === 'mainLocation' && (
|
||||
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.content }} />
|
||||
|
||||
// <div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.content }} />
|
||||
|
||||
<div className="p-4 bg-white shadow rounded-lg">
|
||||
<ReactMarkdown
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
img: ({ node, ...props }) => (
|
||||
<img {...props} className="max-w-full h-auto my-4" alt="" />
|
||||
),
|
||||
a: ({ node, ...props }) => (
|
||||
<a {...props} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline" />
|
||||
)
|
||||
}}
|
||||
>
|
||||
{location.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'backupLocation' && location.backupLocationContent && (
|
||||
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.backupLocationContent }} />
|
||||
@ -98,6 +122,8 @@ const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const common = require("../../../src/helpers/common");
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
@ -106,7 +132,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const messages = (await import(`../../../content/i18n/${locale}.json`)).default;
|
||||
|
||||
const t = createTranslator({ locale, messages });
|
||||
// Function to replace placeholders in HTML content
|
||||
// Function to replace placeholders in HTML content. Placeholders are in the format {key}
|
||||
const replacePlaceholders = (content: string) => {
|
||||
if (!content) return '';
|
||||
const placeholderPattern = /{([^}]+)}/g;
|
||||
@ -121,22 +147,25 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
return `[${locale}:${key}]`;
|
||||
} catch (error) {
|
||||
// Return formatted placeholder on error
|
||||
return `[${locale}:${key}]`;
|
||||
return `[${locale}:'content.'${key}]`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const { data: location } = await axios.get(
|
||||
`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations/${context.params.id}`
|
||||
);
|
||||
const prismaClient = common.getPrismaClient();
|
||||
const location = await getLocation(prismaClient, context);
|
||||
location.content = replacePlaceholders(location.content);
|
||||
location.title = t("content.location." + location.name + ".title");
|
||||
|
||||
if (location.backupLocationId !== null) {
|
||||
const { data: backupLocation } = await axios.get(
|
||||
process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + location.backupLocationId
|
||||
);
|
||||
// const { data: backupLocation } = await axios.get(
|
||||
// process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + location.backupLocationId
|
||||
// );
|
||||
const backupLocation = await prismaClient.location.findFirst({
|
||||
where: {
|
||||
id: location.backupLocationId,
|
||||
},
|
||||
});
|
||||
location.backupLocationName = backupLocation.name;
|
||||
location.backupLocationContent = backupLocation ? replacePlaceholders(backupLocation.content) : "";
|
||||
location.backupLocationImages = backupLocation ? [backupLocation.picture1, backupLocation.picture2, backupLocation.picture3].filter(Boolean) : [];
|
||||
@ -151,4 +180,30 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
};
|
||||
};
|
||||
|
||||
async function getLocation(prismaClient, context) {
|
||||
// Try to parse the ID as a number
|
||||
const numericId = parseInt(context.params.id);
|
||||
let location;
|
||||
|
||||
// If it's a valid number, search by ID
|
||||
if (!isNaN(numericId)) {
|
||||
location = await prismaClient.location.findFirst({
|
||||
where: {
|
||||
id: numericId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no location found by ID or if ID is not numeric, search by name
|
||||
if (!location) {
|
||||
location = await prismaClient.location.findFirst({
|
||||
where: {
|
||||
name: context.params.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
export default ViewLocationPage;
|
||||
|
@ -43,7 +43,7 @@ export default LocationsPage;
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
const { data: items } = await axios.get("/api/data/locations");
|
||||
//console.log('get server props - locations:' + items.length);
|
||||
console.log('get server props - locations:' + items.length);
|
||||
//console.log(items);
|
||||
return {
|
||||
props: {
|
||||
|
@ -1,6 +1,12 @@
|
||||
// Next.js page to show all locations in the database with a link to the location page
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState, useRef, use } from "react";
|
||||
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
// import { getSession } from 'next-auth/client'
|
||||
// import { NextAuth } from 'next-auth/client'
|
||||
import { Publisher, UserRole } from "@prisma/client";
|
||||
@ -26,6 +32,7 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
|
||||
const [filterIsImported, setFilterIsImported] = useState({
|
||||
checked: false,
|
||||
indeterminate: true,
|
||||
@ -113,10 +120,22 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
// Combine name and email filters, removing duplicates
|
||||
let filteredPublishers = [...new Set([...filteredPublishersByName, ...filteredPublishersByEmail])];
|
||||
|
||||
// inactive publishers filter
|
||||
filteredPublishers = showZeroShiftsOnly
|
||||
? filteredPublishers.filter(p => p.assignments.length === 0)
|
||||
: filteredPublishers;
|
||||
// Zero shifts (inactive) and date filter
|
||||
if (showZeroShiftsOnly && selectedDate) {
|
||||
filteredPublishers = publishers.filter(publisher => {
|
||||
// If no assignments at all, include in results
|
||||
if (publisher.assignments.length === 0) return true;
|
||||
|
||||
// Only include publishers who don't have any assignments after the selected date
|
||||
return !publisher.assignments.some(assignment => {
|
||||
const shiftDate = dayjs(assignment.shift.startTime);
|
||||
return shiftDate.isAfter(selectedDate);
|
||||
});
|
||||
});
|
||||
} else if (showZeroShiftsOnly) {
|
||||
// If checkbox is checked but no date selected, show publishers with no assignments
|
||||
filteredPublishers = publishers.filter(publisher => publisher.assignments.length === 0);
|
||||
}
|
||||
|
||||
// trained filter
|
||||
if (flterNoTraining) {
|
||||
@ -124,9 +143,15 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
}
|
||||
|
||||
setShownPubs(filteredPublishers);
|
||||
}, [filter, showZeroShiftsOnly, flterNoTraining]);
|
||||
}, [filter, showZeroShiftsOnly, selectedDate, flterNoTraining]);
|
||||
|
||||
|
||||
// Separate effect to handle date reset when checkbox is unchecked
|
||||
useEffect(() => {
|
||||
if (!showZeroShiftsOnly) {
|
||||
setSelectedDate(null);
|
||||
}
|
||||
}, [showZeroShiftsOnly]);
|
||||
|
||||
const renderPublishers = () => {
|
||||
if (shownPubs.length === 0) {
|
||||
@ -172,20 +197,34 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
|
||||
const exportPublishers = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get('/api/?action=exportPublishersExcel');
|
||||
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
const response = await axiosInstance.get('/api/?action=exportPublishersExcel', { responseType: 'arraybuffer' });
|
||||
|
||||
// Get filename from Content-Disposition header
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let filename = 'publishers.xlsx'; // default fallback
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);
|
||||
if (filenameMatch) {
|
||||
filename = decodeURI(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'publishers.xlsx';
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url); // Clean up
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify(error)); // Log the error
|
||||
console.error(JSON.stringify(error));
|
||||
toast.error("Грешка при експорт на данни");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||
@ -220,7 +259,7 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
</div>
|
||||
{/* export by calling excel helper .ExportPublishersToExcel() */}
|
||||
<div className="flex justify-center m-4">
|
||||
<button className="btn" onClick={exportPublishers}>Export to Excel</button>
|
||||
<button className="button m-2 btn btn-primary" onClick={exportPublishers}>Export to Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -239,6 +278,36 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
/>
|
||||
<span className="ml-2">само без смени</span>
|
||||
</label>
|
||||
{showZeroShiftsOnly && (
|
||||
<div className="flex items-center space-x-2 ml-2">
|
||||
<span className="whitespace-nowrap">след:</span>
|
||||
<DatePicker
|
||||
value={selectedDate}
|
||||
onChange={(newDate) => setSelectedDate(newDate)}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
sx: {
|
||||
width: '140px',
|
||||
'& .MuiInputBase-input': {
|
||||
padding: '4px 8px',
|
||||
fontSize: '0.875rem'
|
||||
}
|
||||
}
|
||||
},
|
||||
mobileWrapper: {
|
||||
sx: {
|
||||
'& .MuiPickersLayout-root': {
|
||||
minWidth: 'unset'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
format="DD.MM.YYYY"
|
||||
className="min-w-[120px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label htmlFor="filterTrained" className="ml-4 inline-flex items-center">
|
||||
<input type="checkbox" id="filterTrained" name="filterTrained"
|
||||
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Location`
|
||||
ADD COLUMN `isOnMainMenu` BOOLEAN NOT NULL DEFAULT false;
|
@ -231,6 +231,7 @@ model Location {
|
||||
backupLocationId Int?
|
||||
backupLocation Location? @relation("BackupLocation", fields: [backupLocationId], references: [id])
|
||||
BackupForLocations Location[] @relation("BackupLocation")
|
||||
isOnMainMenu Boolean @default(false)
|
||||
|
||||
@@map("Location")
|
||||
}
|
||||
|
BIN
public/content/permits/01- Разрешително за януари 25г.pdf
Normal file
BIN
public/content/permits/01- Разрешително за януари 25г.pdf
Normal file
Binary file not shown.
BIN
public/content/permits/12- Разрешително за Декември 24г..pdf
Normal file
BIN
public/content/permits/12- Разрешително за Декември 24г..pdf
Normal file
Binary file not shown.
BIN
public/content/warehouse/1.jpg
Normal file
BIN
public/content/warehouse/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 234 KiB |
BIN
public/content/warehouse/2.jpg
Normal file
BIN
public/content/warehouse/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 MiB |
BIN
public/content/warehouse/3.jpg
Normal file
BIN
public/content/warehouse/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
BIN
public/content/warehouse/4.jpg
Normal file
BIN
public/content/warehouse/4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
BIN
public/content/warehouse/5.jpg
Normal file
BIN
public/content/warehouse/5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
BIN
public/content/warehouse/6.jpg
Normal file
BIN
public/content/warehouse/6.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 862 KiB |
119
public/content/warehouse/index.html
Normal file
119
public/content/warehouse/index.html
Normal file
@ -0,0 +1,119 @@
|
||||
<p><strong>1. Катинар.</strong> Шифъра на катинара е: 1914.</p>
|
||||
<p><br /></p>
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/1.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>2. Място за съхранение</strong> (Подлеза на метростанция Сердика
|
||||
в близост до църквата "Света Петка Самарджийска")
|
||||
</p>
|
||||
<p>
|
||||
Линк: <a
|
||||
href="https://maps.app.goo.gl/FfzZww2EfxeP2y9S7"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
style="color: rgb(17, 85, 204)"
|
||||
>https://maps.app.goo.gl/FfzZww2EfxeP2y9S7</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/2.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/3.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/4.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
<p><br /></p>
|
||||
<p><br /></p>
|
||||
<p>
|
||||
<strong>3. Позицията на щандовете</strong>, начин на съхранение, както и
|
||||
мястото, може да видите в прикачените снимки.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
* Вратата на гардероба е винаги отворена. Няма нужда да натискаме бутона за
|
||||
отваряне
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/5.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src="/content/warehouse/6.jpg"
|
||||
class="w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p><br /></p>
|
||||
<p><br /></p>
|
||||
<p><br /></p>
|
||||
<p><br /></p>
|
||||
<p>
|
||||
<strong>4</strong>. <strong
|
||||
>Зареждането и почистване на щандовете трябва да става извън мястото за
|
||||
съхранение</strong
|
||||
>. Също когато прибираме щандовете е необходимо да са почистени и в добър
|
||||
външен вид с калъф на тях(гумите/колелата да са почистени от кал за да не
|
||||
оставяме следи по пода)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Важно: </strong>посоката на плакатите и на двата щанда трябва да
|
||||
е към стената! Целта е да не се виждат, тъй като <strong
|
||||
>в помещението са забранени рекламни и други материали извън дейността на
|
||||
фирмата.</strong
|
||||
>
|
||||
</p>
|
||||
<p><br /></p>
|
||||
<p>
|
||||
<strong>5. Литература. </strong>Ще има метална кутия за съхранение на
|
||||
литература в непосредствена близост над щандовете на единия от сейфовете.
|
||||
</p>
|
||||
<p>
|
||||
Моля бъдете особено внимателни когато сваляте и качвате кутията за да не се
|
||||
нараните.
|
||||
</p>
|
||||
<p><br /></p>
|
||||
<p>
|
||||
<strong>6. Престой.</strong> Хубаво ще е да не се задържаме прекалено
|
||||
дълго в помещението, повече от необходимо да вземем количките и нужнаta
|
||||
литература.
|
||||
</p>
|
||||
|
||||
**1. Катинар.** Шифъра на катинара е: 1914.\n **2.
|
||||
Място за съхранение** (Подлеза на метростанция Сердика в близост до църквата
|
||||
"Света Петка Самарджийска")\nЛинк:
|
||||
[https://maps.app.goo.gl/FfzZww2EfxeP2y9S7](https://maps.app.goo.gl/FfzZww2EfxeP2y9S7)\n\n\n\n
|
||||
**3. Позицията на щандовете**, начин на съхранение, както и мястото, може да
|
||||
видите в прикачените снимки.\n**\* Вратата на гардероба е винаги отворена. Няма
|
||||
нужда да натискаме бутона за
|
||||
отваряне**\n\n **4.**
|
||||
**Зареждането и почистване на щандовете трябва да става извън мястото за
|
||||
съхранение**. Също когато прибираме щандовете е необходимо да са почистени и в
|
||||
добър външен вид с калъф на тях(гумите/колелата да са почистени от кал за да не
|
||||
оставяме следи по пода)\n**Важно:** посоката на плакатите и на двата щанда
|
||||
трябва да е към стената! Целта е да не се виждат, тъй като **в помещението са
|
||||
забранени рекламни и други материали извън дейността на фирмата.** **5.
|
||||
Литература.** Ще има метална кутия за съхранение на литература в непосредствена
|
||||
близост над щандовете на единия от сейфовете. Моля бъдете особено внимателни
|
||||
когато сваляте и качвате кутията за да не се нараните. **6. Престой.** Хубаво ще
|
||||
е да не се задържаме прекалено дълго в помещението, повече от необходимо да
|
||||
вземем количките и нужнаta литература.
|
@ -89,7 +89,7 @@ exports.getPrismaClient = function getPrismaClient() {
|
||||
logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE);
|
||||
prisma = new PrismaClient({
|
||||
// Optional: Enable Prisma logging
|
||||
//log: ['query', 'info', 'warn', 'error'],
|
||||
log: ['query', 'info', 'warn', 'error'],
|
||||
datasources: { db: { url: process.env.DATABASE } },
|
||||
});
|
||||
}
|
||||
|
@ -616,7 +616,7 @@ exports.ReadDocxFileForMonth = async function (filePath, buffer, month, year, pr
|
||||
};
|
||||
|
||||
|
||||
exports.ExportPublishersToExcel = async function (req, res) {
|
||||
exports.generatePublishersExcel = async function () {
|
||||
const prisma = common.getPrismaClient();
|
||||
const publishers = await prisma.publisher.findMany({
|
||||
// where: { isActive: true, },
|
||||
@ -624,9 +624,9 @@ exports.ExportPublishersToExcel = async function (req, res) {
|
||||
// availabilities: { where: { isActive: true, }, },
|
||||
// assignments: { include: { shift: true, }, },
|
||||
congregation: true,
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
const ExcelJS = require("exceljs");
|
||||
const xjswb = new ExcelJS.Workbook();
|
||||
const sheet = xjswb.addWorksheet("Publishers");
|
||||
@ -639,10 +639,11 @@ exports.ExportPublishersToExcel = async function (req, res) {
|
||||
{ header: "Congregation", key: "congregationName", width: 32 },
|
||||
{ header: "Last Login", key: "lastLogin", width: 32 },
|
||||
{ header: "Type", key: "PublisherTypeText", width: 32 },
|
||||
{ header: "Active", key: "isActive", width: 10 },
|
||||
// { header: "Active", key: "isActive", width: 10 },
|
||||
{ header: "Created At", key: "createdAt", width: 32 },
|
||||
{ header: "Updated At", key: "updatedAt", width: 32 },
|
||||
];
|
||||
|
||||
publishers.forEach((publisher) => {
|
||||
sheet.addRow({
|
||||
name: publisher.firstName + " " + publisher.lastName,
|
||||
@ -650,18 +651,17 @@ exports.ExportPublishersToExcel = async function (req, res) {
|
||||
email: publisher.email,
|
||||
phone: publisher.phone,
|
||||
role: publisher.role,
|
||||
congregationName: publisher.congregation.name,
|
||||
congregationName: publisher.congregation?.name || 'не е зададен', // Add null check here
|
||||
lastLogin: publisher.lastLogin,
|
||||
PublisherTypeText: publisher.PublisherTypeText,
|
||||
isActive: publisher.isActive,
|
||||
// isActive: publisher.isActive,
|
||||
createdAt: publisher.createdAt,
|
||||
updatedAt: publisher.updatedAt,
|
||||
|
||||
});
|
||||
});
|
||||
res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
res.setHeader("Content-Disposition", "attachment; filename=" + encodeURI("Publishers.xlsx"));
|
||||
xjswb.xlsx.write(res);
|
||||
|
||||
// Return a buffer with the Excel data
|
||||
return await xjswb.xlsx.writeBuffer();
|
||||
}
|
||||
|
||||
const weekNames = [
|
||||
|
Reference in New Issue
Block a user