1 Commits

Author SHA1 Message Date
slasherss 51d063b0e5 removed file upload dialog
removes file upload dialog from frontend, upload from backend along with parsing dependencies
2026-05-01 22:34:26 +02:00
16 changed files with 11 additions and 349 deletions
+3 -133
View File
@@ -15,9 +15,7 @@
"connect-mongo": "^5.0.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"fuse.js": "^7.3.0",
"mongoose": "^7.4.1",
"multer": "^2.0.2",
"node-schedule": "^2.1.1",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
@@ -25,8 +23,7 @@
"speakeasy": "^2.0.0",
"tx2": "^1.0.5",
"underscore": "^1.13.8",
"web-push": "^3.6.7",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz"
"web-push": "^3.6.7"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
@@ -34,7 +31,6 @@
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/multer": "^1.4.11",
"@types/node-schedule": "^2.1.7",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
@@ -272,15 +268,6 @@
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true
},
"node_modules/@types/multer": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
"integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "20.4.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
@@ -788,11 +775,6 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"node_modules/array-differ": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
@@ -1041,18 +1023,8 @@
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/bytes": {
"version": "3.1.2",
@@ -1294,21 +1266,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/connect-history-api-fallback": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
@@ -1951,19 +1908,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fuse.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz",
"integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/krisk"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2965,25 +2909,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/multer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.0.tgz",
"integrity": "sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/multi-stage-sourcemap": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.3.1.tgz",
@@ -3484,20 +3409,6 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -3925,23 +3836,6 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-template": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz",
@@ -4196,12 +4090,6 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
@@ -4293,12 +4181,6 @@
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -4598,18 +4480,6 @@
"node": ">=0.10.0"
}
},
"node_modules/xlsx": {
"version": "0.20.1",
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz",
"integrity": "sha512-hA7SYmn/H3cJ1VGi7kmSKxbEXCEn5UVJlwYFSFXhLe54wPUvW+80TmR5l+TZh4nJCw8G/e0SuZUfvqlTIh40hw==",
"license": "Apache-2.0",
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+1 -5
View File
@@ -17,9 +17,7 @@
"connect-mongo": "^5.0.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"fuse.js": "^7.3.0",
"mongoose": "^7.4.1",
"multer": "^2.0.2",
"node-schedule": "^2.1.1",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
@@ -27,8 +25,7 @@
"speakeasy": "^2.0.0",
"tx2": "^1.0.5",
"underscore": "^1.13.8",
"web-push": "^3.6.7",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz"
"web-push": "^3.6.7"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
@@ -36,7 +33,6 @@
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/multer": "^1.4.11",
"@types/node-schedule": "^2.1.7",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
-35
View File
@@ -1,35 +0,0 @@
import User, { IUser } from "@/schemas/User";
import Fuse, { FuseIndex } from "fuse.js";
import { Types } from "mongoose";
class CachedIndex {
private index: FuseIndex = null
private list: (IUser & { _id: Types.ObjectId; })[] = null
private fuse: Fuse<IUser & { _id: Types.ObjectId; }> = null
refresh() {
this.index = null
this.list = null
this.fuse = null
}
private async getIndex() {
return this.index || (this.index = Fuse.createIndex(['uname', 'fname', 'surname', 'room'], await this.getList()))
}
private async getList() {
return this.list || (this.list = await User.find({}, { uname: 1, fname: 1, surname: 1, room: 1 }))
}
private async getFuse(): Promise<typeof this.fuse> {
return this.fuse || (this.fuse = new Fuse(await this.getList(), {
keys: ['uname', 'fname', 'surname', 'room']
}, await this.getIndex()))
}
async search(q: string) {
return (await this.getFuse()).search(q, { limit: 5 }).map(r => r.item)
}
}
export default new CachedIndex()
+2 -2
View File
@@ -8,9 +8,9 @@ import { notifRouter } from "./notif";
import { keysRouter } from "./keys";
import { cleanRouter } from "./clean";
import { settingsRouter } from "./settings";
import User from "@/schemas/User";
import Group from "@/schemas/Group";
import usettings from "@/helpers/usettings";
import CachedIndex from "@/helpers/CachedIndex";
export const adminRouter = Router()
@@ -25,7 +25,7 @@ adminRouter.use('/clean', cleanRouter)
adminRouter.use('/settings', settingsRouter)
adminRouter.get('/usearch', async (req, res) => {
var results = await CachedIndex.search(req.query['q'].toString())
var results = await User.find({$text: {$search: req.query['q'].toString()}}, {uname: 1, surname: 1, fname: 1, room: 1})
res.send(results)
})
-56
View File
@@ -1,24 +1,11 @@
import { Router } from "express"
import { Perms, adminPerm, project } from "@/utility"
import multer from "multer"
import * as XLSX from "xlsx"
import Menu from "@schemas/Menu"
import { editorRouter } from "./editor"
import usettings, { Features } from "@/helpers/usettings"
const menuRouter = Router()
const storage = multer.memoryStorage()
const upload = multer({storage: storage})
interface sheetObject {
day: string;
sn: string;
sn2: string;
ob: string;
kol: string;
}
menuRouter.use(adminPerm(Perms.Menu))
menuRouter.use(usettings.mw(Features.Menu))
@@ -120,49 +107,6 @@ menuRouter.post('/:start/:count', async (req, res) => {
}
})
menuRouter.post('/upload', upload.single('menu'), async (req,res)=> {
if (!req.file) {
res.status(400).send("File required")
return
}
var sheet = XLSX.read(req.file.buffer)
var data: sheetObject[] = XLSX.utils.sheet_to_json(sheet.Sheets[sheet.SheetNames[0]], {raw: true, header: ["day", "sn", "sn2", "ob", "kol"], range: 2})
data = data.map((val, i) => {
var result: any = {}
var dateparts = val.day.match(/(\d{2})\.(\d{2})\.(\d{4})/)
if (dateparts) {
var date = new Date(parseInt(dateparts[3]),parseInt(dateparts[2])-1,parseInt(dateparts[1]))
date.setUTCHours(0,0,0,0)
} else {
res.sendStatus(500)
return
}
result.day = date
result.sn = val.sn.split('\n')
result.sn.push(val.sn2)
//#region ob
var ob = val.ob
ob = ob.replace(/^\s*\n/gm, "")
ob = ob.replace(/ \/ /gm, "\n")
ob = ob.replace(/:(?!\s)/gm, ": ")
result.ob = ob.split('\n')
//#endregion
result.kol = val.kol
if (date.getUTCDay() == 5) {
result.kol = null
}
return result
})
if (await Menu.insertMany(data)) {
res.status(201).send({status: 201})
} else {
res.sendStatus(500)
}
})
menuRouter.put('/:id', async (req, res) => {
let menu = await Menu.findById(req.params.id)
if (!menu) {
-3
View File
@@ -1,4 +1,3 @@
import CachedIndex from "@/helpers/CachedIndex";
import { Perms, project } from "@/utility";
import mongoose, { Types, Schema } from "mongoose"
@@ -30,8 +29,6 @@ const userSchema = new Schema<IUser>({
userSchema.index({uname: "text", room: "text", fname: "text", surname: "text"}, {weights: {fname: 3, surname: 4, room: 2, uname: 1}, default_language: "none"})
userSchema.post("save", () => {CachedIndex.refresh()})
export default mongoose.model("logins", userSchema)
export function userVisibleFields(user: IUser & {_id: mongoose.Types.ObjectId}): Pick<IUser, "fname" | "surname" | "uname" | "room"> & {_id: mongoose.Types.ObjectId} {
@@ -3,7 +3,6 @@
<mat-radio-group [(ngModel)]="type">
<mat-radio-button value="day">Dzień</mat-radio-button>
<mat-radio-button value="week">Tydzień</mat-radio-button>
<mat-radio-button value="file">Plik</mat-radio-button>
</mat-radio-group>
<div>
@switch (type) {
@@ -21,9 +20,6 @@
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
}
@case ("file") {
<button mat-flat-button (click)="activateUpload()">Otwórz okno</button>
}
}
</div>
</mat-dialog-content>
@@ -0,0 +1,3 @@
mat-dialog-content {
padding-bottom: 5px !important;
}
@@ -1,6 +1,5 @@
import { Component, inject, signal } from '@angular/core'
import { MenuUploadComponent } from '../menu-upload/menu-upload.component'
import { MatDialog, MatDialogRef } from '@angular/material/dialog'
import { MatDialogRef } from '@angular/material/dialog'
import { FDSelection } from 'src/app/fd.da'
import { FormControl, FormGroup } from '@angular/forms'
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker'
@@ -18,7 +17,6 @@ import { weekendFilter } from 'src/app/util'
})
export class MenuAddComponent {
public dialogRef: MatDialogRef<MenuAddComponent> = inject(MatDialogRef)
private dialog = inject(MatDialog)
type: string | undefined
filter = weekendFilter
@@ -45,15 +43,4 @@ export class MenuAddComponent {
break
}
}
activateUpload() {
this.dialog
.open(MenuUploadComponent)
.afterClosed()
.subscribe(data => {
if (data) {
this.dialogRef.close({ type: 'file', ...data })
}
})
}
}
@@ -5,7 +5,6 @@ import { FDSelection } from 'src/app/fd.da'
import { Menu } from 'src/app/types/menu'
import { MatTableDataSource } from '@angular/material/table'
import { MatDialog } from '@angular/material/dialog'
import { MenuUploadComponent } from './menu-upload/menu-upload.component'
import { Status } from 'src/app/types/status'
import { MatSnackBar } from '@angular/material/snack-bar'
import { MenuAddComponent } from './menu-add/menu-add.component'
@@ -121,15 +120,6 @@ export class MenuEditComponent implements OnDestroy {
if (s.status.toString().match(/2\d\d/)) this.refresh()
}
activateUpload() {
this.dialog
.open(MenuUploadComponent)
.afterClosed()
.subscribe(data => {
if (data) this.refresh()
})
}
editSn(id: string) {
this.ac
.editSn(id, this.dataSource.data.find(v => v._id == id)!.sn)
@@ -1,20 +0,0 @@
<h1 mat-dialog-title>Import z pliku</h1>
<mat-dialog-content>
<input type="file" name="menu" #fu style="display: none;" (change)="onFileChange($event)" accept=".xlsx,.ods,application/vnd.oasis.opendocument.spreadsheet">
<button matButton="outlined" (click)="fu.click()">Wybierz plik</button>
<div style="color: red;">
<h1>UWAGA!</h1>
<h3>Przed wysłaniem upewnij się że</h3>
<ul>
<li>Daty w pliku są prawidłowe i poprawnie sformatowane (DD.MM.RRRR)</li>
<li>Wszystkie pozycje w menu są w osobnych linijkach</li>
<li>Załączony dokument to arkusz w formacie XLSX lub ODS</li>
</ul>
<h2>Nie spełnienie któregokolwiek z tych wymagań może skutkować szkodami w programie!</h2>
<h3>Późniejsza modyfikacja danych jest niemożliwa w tej wersji programu.</h3>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button matButton="outlined" class="error-color" [disabled]="file === undefined" (click)="submit()">Wyślij</button>
<button mat-button mat-dialog-close>Anuluj</button>
</mat-dialog-actions>
@@ -1,4 +0,0 @@
:host {
margin: 8pt;
display: block;
}
@@ -1,29 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { MenuUploadComponent } from './menu-upload.component'
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'
import { MenuEditService } from '../menu-edit.service'
xdescribe('MenuUploadComponent', () => {
let component: MenuUploadComponent
let fixture: ComponentFixture<MenuUploadComponent>
beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService')
TestBed.configureTestingModule({
declarations: [MenuUploadComponent],
providers: [
{ provide: MenuEditService, useValue: acMock },
{ provide: MatDialogRef, useValue: {} },
],
imports: [MatDialogModule],
})
fixture = TestBed.createComponent(MenuUploadComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
@@ -1,30 +0,0 @@
import { Component, inject } from '@angular/core'
import { MatDialogRef } from '@angular/material/dialog'
import { MenuEditService } from '../menu-edit.service'
@Component({
selector: 'app-upload-edit',
templateUrl: './menu-upload.component.html',
styleUrls: ['./menu-upload.component.scss'],
standalone: false,
})
export class MenuUploadComponent {
private ac = inject(MenuEditService)
public dialogRef: MatDialogRef<MenuUploadComponent> = inject(MatDialogRef)
protected file: File | undefined
onFileChange(event: Event) {
const file: File = (event.target as HTMLInputElement).files![0]
if (file) {
this.file = file
} else {
this.file = undefined
}
}
submit() {
this.ac.postMenu(this.file!)?.subscribe(value => {
this.dialogRef.close(value)
})
}
}
-2
View File
@@ -26,7 +26,6 @@ import { LogoutConfirmationComponent } from './app-view/personal/logout-confirma
import { PersonalComponent } from './app-view/personal/personal.component'
import { AppComponent } from './app.component'
import { LoginComponent } from './login/login.component'
import { MenuUploadComponent } from './admin-view/menu-edit/menu-upload/menu-upload.component'
import { NewsEditComponent } from './admin-view/news-edit/news-edit.component'
import { MatSnackBarModule } from '@angular/material/snack-bar'
import { NewPostComponent } from './admin-view/news-edit/new-post/edit-post.component'
@@ -109,7 +108,6 @@ import { MatTooltipModule } from "@angular/material/tooltip";
LogoutConfirmationComponent,
DateSelectorComponent,
AdminViewComponent,
MenuUploadComponent,
NewsEditComponent,
NewPostComponent,
AccountMgmtComponent,
@@ -10,7 +10,6 @@ import {
withInterceptorsFromDi,
} from '@angular/common/http'
import { environment } from 'src/environments/environment'
import { firstValueFrom } from 'rxjs'
describe('UpdatesService', () => {
let service: UpdatesService
@@ -33,7 +32,7 @@ describe('UpdatesService', () => {
})
it('should grab news', () => {
const res = firstValueFrom(service.getNews())
const res = service.getNews()
const req = httpTestingController.expectOne(environment.apiEndpoint+`/app/news`)
expect(res).withContext("empty news array").toBeDefined()