From 0b8a93b256c5289a42ae1a0a4af679f34fae93ba Mon Sep 17 00:00:00 2001 From: ewandor Date: Tue, 10 Jan 2023 18:48:47 +0100 Subject: [PATCH] List and Read on frontend, fullecrud on back --- .gitignore | 2 +- back/.gitignore | 160 ++++++++++++++++++ .../core/__pycache__/__init__.cpython-310.pyc | Bin 121 -> 0 bytes .../core/__pycache__/routes.cpython-310.pyc | Bin 2545 -> 0 bytes .../core/__pycache__/schemas.cpython-310.pyc | Bin 599 -> 0 bytes back/app/core/routes.py | 40 ++++- .../entity/__pycache__/models.cpython-310.pyc | Bin 876 -> 0 bytes .../__pycache__/schemas.cpython-310.pyc | Bin 822 -> 0 bytes back/app/entity/models.py | 3 + back/app/entity/schemas.py | 3 +- back/app/main.py | 1 - .../user/__pycache__/manager.cpython-310.pyc | Bin 2505 -> 0 bytes .../user/__pycache__/routes.cpython-310.pyc | Bin 3178 -> 0 bytes back/app/user/manager.py | 48 +++++- back/app/user/routes.py | 46 ++--- back/requirements.txt | 1 + front/app/src/app/app.module.ts | 4 +- front/app/src/app/custom/chtlogin.service.ts | 41 +++++ .../routes/clients/card/card.component.css | 0 .../routes/clients/card/card.component.html | 28 +++ .../clients/card/card.component.spec.ts | 25 +++ .../app/routes/clients/card/card.component.ts | 48 ++++++ .../routes/clients/clients-routing.module.ts | 14 ++ .../src/app/routes/clients/clients.module.ts | 20 +++ .../src/app/routes/clients/clients.service.ts | 23 +++ .../routes/clients/list/list.component.css | 0 .../routes/clients/list/list.component.html | 43 +++++ .../clients/list/list.component.spec.ts | 25 +++ .../app/routes/clients/list/list.component.ts | 84 +++++++++ .../src/app/routes/routes-routing.module.ts | 1 + front/app/src/assets/data/menu.json | 6 + nginx/nginx.conf | 8 + 32 files changed, 623 insertions(+), 51 deletions(-) create mode 100644 back/.gitignore delete mode 100644 back/app/core/__pycache__/__init__.cpython-310.pyc delete mode 100644 back/app/core/__pycache__/routes.cpython-310.pyc delete mode 100644 back/app/core/__pycache__/schemas.cpython-310.pyc delete mode 100644 back/app/entity/__pycache__/models.cpython-310.pyc delete mode 100644 back/app/entity/__pycache__/schemas.cpython-310.pyc delete mode 100644 back/app/user/__pycache__/manager.cpython-310.pyc delete mode 100644 back/app/user/__pycache__/routes.cpython-310.pyc create mode 100644 front/app/src/app/custom/chtlogin.service.ts create mode 100644 front/app/src/app/routes/clients/card/card.component.css create mode 100644 front/app/src/app/routes/clients/card/card.component.html create mode 100644 front/app/src/app/routes/clients/card/card.component.spec.ts create mode 100644 front/app/src/app/routes/clients/card/card.component.ts create mode 100644 front/app/src/app/routes/clients/clients-routing.module.ts create mode 100644 front/app/src/app/routes/clients/clients.module.ts create mode 100644 front/app/src/app/routes/clients/clients.service.ts create mode 100644 front/app/src/app/routes/clients/list/list.component.css create mode 100644 front/app/src/app/routes/clients/list/list.component.html create mode 100644 front/app/src/app/routes/clients/list/list.component.spec.ts create mode 100644 front/app/src/app/routes/clients/list/list.component.ts diff --git a/.gitignore b/.gitignore index 1febf6ca..5ddfb87d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .idea/ -__pycache__ +*/__pycache__ diff --git a/back/.gitignore b/back/.gitignore new file mode 100644 index 00000000..6769e21d --- /dev/null +++ b/back/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/back/app/core/__pycache__/__init__.cpython-310.pyc b/back/app/core/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 84b43d663ffed23a8e5d562fda2a2cbcb2d23883..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmd1j<>g`kg7m{XlR@-j5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HFesX?Fs=l6n pVnG3rS(K_DAD@|*SrQ+wS5SG2!zMRBr8Fni4y3D?2}rOo004w37X1JK diff --git a/back/app/core/__pycache__/routes.cpython-310.pyc b/back/app/core/__pycache__/routes.cpython-310.pyc deleted file mode 100644 index cdbf309b13cbf05bc6cac16d75e1bed980cc4961..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2545 zcmaJ@OK%%D5GJ`VNtPYOkrO9P8|4ACi=uWP*T86j7EOS{4Qe+97AY3%CFQQO(kfgk zsFW3uGy$4RdP$Ex$UrX!`V0C~cB2 z?>MXw@()gqHU^W=q33r%aKdRyIy9gZpIOQ}R$z7Pz&3L$bvka~cD%srRDud6!sYg3 z8dPC)V64ID!ngpV2VAA<)7zT<5Zh>b z((u9_bPIa^F9<~*laXE6dz4eo#_TECv-T;so-uA0lsn39+rN@F{?L8+Ak&`b;hXPEE4|s+~^8MJktJ z;pK((7$)m$>rt-uu)Kvol74Xdo?q$S% z$1achmpWB?7^~qX7 zM^GEa>_|YY!pc8Xkel2c+rVT8>W24>{Xz?C>=tYqI7$i&xLw(ti#Ns^;cP+A-vfbZ z^5eoCHiCl+#z~(_R?vbyXZIjAj5AGyq3E%-k2Srk>HRX~$lo&Vwd=_KIP3H_uAp7K z`+H>g*@^BeJvC$Q?ERsAEf?x!$Ve>%*_%L~K{a?#_?Em3gLc}9NV#?(Vf*4W%<>v& zT!n7p?UEXGU%mJpNcM6@#a1^{t#;P$^2-NIFXSpx{XE2c)h-t*N>bmGXUs`hs&aV| zo+Wx$uAo4y+AZ(u+hy9cU@fSZYvDbBH8_rw@*_|~)g1GRJv_@Q)TNbI4cn#+et`DB zh64;exfqm346n}U2{lImJzH&=b>=GYoVPJ{hg`h@lj90PzM>3+kA4Gye{^h5>nwbn zZSG9>6DT3RyD8rXd-4MmJ_;;u!*hUe3T>|TI>-C7)Rpg|f%*C)*I?}v=s8wSQ+wFd zTT4!z*{|N>uhOhvlb9je?q#`}MbSjLgoFN(SlhJK>nn34ymTHGg1V{eP@C}^5Y);U z7MJNx858)VnST|gzDvwOc?ku^+}JCrYVKyr>Z#ZAXb}{1+;(Y`tYbs5U|?Y-FG`AN>cgy@V=?Fj-f;1#QR&N&-12tceE z2+dza9-tpW3;mY&1w>aYZGYh_mU7oQS5{xlL01Ju_JyM}LbVBkW;q9z2f*=l;G#>d zK$}S_TrA~G)mlo|m9n10vLZW?@_wnR?aeDQZLHzpdyo~_E*oa5Y0xw}v&F5hl^r%K zBQT97vji~@=mXgs0PstonBavvFJxXtpRX2 diff --git a/back/app/core/routes.py b/back/app/core/routes.py index 041f51cf..95b4ea27 100644 --- a/back/app/core/routes.py +++ b/back/app/core/routes.py @@ -1,7 +1,11 @@ from beanie import PydanticObjectId +from beanie.odm.enums import SortDirection from fastapi import APIRouter, HTTPException -from typing import TypeVar, List, Generic +from fastapi_paginate import Page, Params, add_pagination +from fastapi_paginate.ext.motor import paginate + +from typing import TypeVar, List, Generic, Any, Dict T = TypeVar('T') @@ -10,7 +14,21 @@ V = TypeVar('V') W = TypeVar('W') +def parse_sort(sort_by): + fields = [] + for field in sort_by.split(','): + dir, col = field.split('(') + fields.append((col[:-1], 1 if dir == 'asc' else -1)) + + return fields + + +def parse_query(query) -> Dict[Any, Any]: + return {} + + def get_crud_router(model, model_create, model_read, model_update): + router = APIRouter() @router.post("/", response_description="{} added to the database".format(model.__name__)) @@ -22,12 +40,17 @@ def get_crud_router(model, model_create, model_read, model_update): @router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) async def read_id(id: PydanticObjectId) -> model_read: item = await model.get(id) - return item + return model_read(**item.dict()) - @router.get("/", response_description="{} records retrieved".format(model.__name__)) - async def read_list() -> List[model_read]: - item = await model.find_all().to_list() - return item + @router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__)) + async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None) -> Page[model_read]: + sort = parse_sort(sort_by) + query = parse_query(query) + # limit=limit, skip=offset, + + collection = model.get_motor_collection() + items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort) + return await items @router.put("/{id}", response_description="{} record updated".format(model.__name__)) async def update(id: PydanticObjectId, req: model_update) -> model_read: @@ -44,7 +67,7 @@ def get_crud_router(model, model_create, model_read, model_update): ) await item.update(update_query) - return item + return model_read(**item.dict()) @router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) async def delete(id: PydanticObjectId) -> dict: @@ -61,4 +84,7 @@ def get_crud_router(model, model_create, model_read, model_update): "message": "{} deleted successfully".format(model.__name__) } + add_pagination(router) return router + + diff --git a/back/app/entity/__pycache__/models.cpython-310.pyc b/back/app/entity/__pycache__/models.cpython-310.pyc deleted file mode 100644 index be364450c834fd15e3f4614cd9e1d4d2acf0aea0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 876 zcmZuw&2H2%5VjL1>&~jI7s#di0xQ%5Ew{b!bFh#sH{(h{iIX}GXwU7HcYq@o z9)L&j5l*}UCuWi&DuJzhGyeS9W6wC<(NV#$U48vledmn*puydi;oua%;SB;X;0Tq~<=xr=&2le|JM$^Pq)%m{jCe)patJ)2y8fmaE^C-1;rnOqfLhH7Ju_1r0 z^%X9iXZ!-=dP%TE_`>`V`2@e7ZmSOL_GD}Nev604_*>k?Hp5AO@Ht9o-9R9j;DSp& zvHx6BpyHIIZxQ}n@-?%m6~K4QW@r<%G)`}=K#QsWBtK7u_?W0A7DB*c=HE#yk3_bZ z*yWBvfr(nhd`?tMbq!Gj%9o(4#)Sd!b{NjfBrd>Snz0E{y0K}@MT1GLyv53aHUR~j zTYaxU0{lo4FCWgy!guMo# zLpItg5K-l3V8nu@D%Z2i_N!MOmdl*s`1bv#dBz#LC2&3>1Y0x*=zsxlSw*vGdB-cx zDVDA1WF;|fAT{x4S*3^-WQa2(5i7(wED$dO&VavXWpRfu zv0cf%0zH{&-x-XvSK67k1DN&>X|huI7&TJ;n{PAZpH# z51}&m(c!QMD-cv_Jc7n{G?ujW3AWO-&c+CW1fdV1j&S&2?|8MDKfp`+KkflK#R^gI Ll~~DD^d$KS6)&Gx diff --git a/back/app/entity/models.py b/back/app/entity/models.py index 4bf22748..c5f3c0c7 100644 --- a/back/app/entity/models.py +++ b/back/app/entity/models.py @@ -18,3 +18,6 @@ class Entity(Document): address: str created_at: datetime = Field(default=datetime.utcnow(), nullable=False) updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False) + # + # class Settings: + # name = "entities" diff --git a/back/app/entity/schemas.py b/back/app/entity/schemas.py index fb04cc64..b9f1d072 100644 --- a/back/app/entity/schemas.py +++ b/back/app/entity/schemas.py @@ -4,13 +4,14 @@ from datetime import datetime from pydantic import BaseModel from .models import Entity, EntityType +from ..core.schemas import Writer class EntityRead(Entity): pass -class EntityCreate(BaseModel): +class EntityCreate(Writer): type: EntityType name: str address: str diff --git a/back/app/main.py b/back/app/main.py index e8d783af..2eb25bd4 100644 --- a/back/app/main.py +++ b/back/app/main.py @@ -1,5 +1,4 @@ from fastapi import FastAPI -from fastapi import Depends, Request from .contract import contract_router from .db import init_db diff --git a/back/app/user/__pycache__/manager.cpython-310.pyc b/back/app/user/__pycache__/manager.cpython-310.pyc deleted file mode 100644 index ef4e4d641014f44adf869017b8ef08588285a2ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2505 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 Le78*e=`#60H^&D- diff --git a/back/app/user/manager.py b/back/app/user/manager.py index 6d9f527d..6dfcad57 100644 --- a/back/app/user/manager.py +++ b/back/app/user/manager.py @@ -1,10 +1,14 @@ import uuid from typing import Any, Dict, Generic, Optional +from bson import ObjectId from fastapi import Depends -from fastapi_users import BaseUserManager, UUIDIDMixin, models, exceptions, schemas +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models, exceptions, schemas +from fastapi_users.authentication import BearerTransport, AuthenticationBackend +from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy + +from .models import User, get_user_db, AccessToken, get_access_token_db -from .models import User, get_user_db SECRET = "SECRET" @@ -66,6 +70,44 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): return created_user + def parse_id(self, value: Any) -> uuid.UUID: + if isinstance(value, ObjectId): + return value + if isinstance(value, uuid.UUID): + return value + try: + return uuid.UUID(value) + except ValueError as e: + raise exceptions.InvalidID() from e + async def get_user_manager(user_db=Depends(get_user_db)): - yield UserManager(user_db) \ No newline at end of file + yield UserManager(user_db) + + +def get_database_strategy( + access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), +) -> DatabaseStrategy: + return DatabaseStrategy(access_token_db, lifetime_seconds=3600) + + +bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") + + +auth_backend = AuthenticationBackend( + name="db", + transport=bearer_transport, + get_strategy=get_database_strategy, +) + + +fastapi_users = FastAPIUsers[User, uuid.UUID]( + get_user_manager, + [auth_backend], +) + +get_current_user = fastapi_users.current_user(active=True) + + +def get_auth_router(): + return fastapi_users.get_auth_router(auth_backend) diff --git a/back/app/user/routes.py b/back/app/user/routes.py index f3445525..cea59e5b 100644 --- a/back/app/user/routes.py +++ b/back/app/user/routes.py @@ -1,45 +1,13 @@ -import uuid -from typing import Optional - -from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models, exceptions -from fastapi_users.authentication import BearerTransport, AuthenticationBackend -from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy +from fastapi import Depends from beanie import PydanticObjectId from fastapi import APIRouter, HTTPException from typing import List -from .models import User, AccessToken, get_user_db, get_access_token_db +from .models import User from .schemas import UserRead, UserUpdate, UserCreate -from .manager import get_user_manager - - -def get_database_strategy( - access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), -) -> DatabaseStrategy: - return DatabaseStrategy(access_token_db, lifetime_seconds=3600) - - -bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") - - -auth_backend = AuthenticationBackend( - name="db", - transport=bearer_transport, - get_strategy=get_database_strategy, -) - - -fastapi_users = FastAPIUsers[User, uuid.UUID]( - get_user_manager, - [auth_backend], -) - - -def get_auth_router(): - return fastapi_users.get_auth_router(auth_backend) +from .manager import get_user_manager, get_current_user, get_auth_router router = APIRouter() @@ -51,10 +19,16 @@ async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> di return {"message": "User added successfully"} +@router.get("/me", response_description="User record retrieved") +async def read_me(user=Depends(get_current_user)) -> UserRead: + user = await User.get(user.id) + return UserRead(**user.dict()) + + @router.get("/{id}", response_description="User record retrieved") async def read_id(id: PydanticObjectId) -> UserRead: user = await User.get(id) - return user + return UserRead(**user.dict()) @router.get("/", response_model=List[UserRead], response_description="User records retrieved") diff --git a/back/requirements.txt b/back/requirements.txt index f016f1e5..f3c74a10 100644 --- a/back/requirements.txt +++ b/back/requirements.txt @@ -2,4 +2,5 @@ fastapi==0.88.0 fastapi_users==10.2.1 fastapi_users_db_beanie==1.1.2 motor==3.1.1 +fastapi-paginate==0.1.0 uvicorn \ No newline at end of file diff --git a/front/app/src/app/app.module.ts b/front/app/src/app/app.module.ts index c0bedb1e..7b1d7111 100644 --- a/front/app/src/app/app.module.ts +++ b/front/app/src/app/app.module.ts @@ -23,7 +23,7 @@ export function TranslateHttpLoaderFactory(http: HttpClient) { } import { LoginService } from '@core/authentication/login.service'; -import { FakeLoginService } from './fake-login.service'; +import { ChtloginService } from '@core/../custom/chtlogin.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ @@ -49,7 +49,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; ], providers: [ { provide: BASE_URL, useValue: environment.baseUrl }, - { provide: LoginService, useClass: FakeLoginService }, // <= Remove it in the real APP + { provide: LoginService, useClass: ChtloginService }, // <= Remove it in the real APP httpInterceptorProviders, appInitializerProviders, ], diff --git a/front/app/src/app/custom/chtlogin.service.ts b/front/app/src/app/custom/chtlogin.service.ts new file mode 100644 index 00000000..c785bc3f --- /dev/null +++ b/front/app/src/app/custom/chtlogin.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { HttpHeaders, HttpParams } from '@angular/common/http'; +import { Token, User } from '../core/authentication/interface'; +import { Menu } from '@core'; +import { map } from 'rxjs/operators'; +import { LoginService } from '../core/authentication/login.service' + +@Injectable({ + providedIn: 'root', +}) +export class ChtloginService extends LoginService { + + login(username: string, password: string, rememberMe = false) { + const body = new HttpParams() + .set('username', username) + .set('password', password); + + return this.http.post('/api/v1/auth/login', body.toString(), { + headers: new HttpHeaders() + .set('Content-Type', 'application/x-www-form-urlencoded') + }); + } + + refresh(params: Record) { + return this.http.post('/api/v1/auth/refresh', params); + } + + logout() { + return this.http.post('/api/v1/auth/logout', {}); + } + + me() { + return this.http.get('/api/v1/users/me'); + } + + menu() { + return this.http + .get<{ menu: Menu[] }>('assets/data/menu.json?_t=' + Date.now()) + .pipe(map(res => res.menu)); + } +} diff --git a/front/app/src/app/routes/clients/card/card.component.css b/front/app/src/app/routes/clients/card/card.component.css new file mode 100644 index 00000000..e69de29b diff --git a/front/app/src/app/routes/clients/card/card.component.html b/front/app/src/app/routes/clients/card/card.component.html new file mode 100644 index 00000000..e2e06e18 --- /dev/null +++ b/front/app/src/app/routes/clients/card/card.component.html @@ -0,0 +1,28 @@ + + + +
+ + Name + + + + + Address + + + + + Type + + Particulier + Entreprise + Institution + + + + +
diff --git a/front/app/src/app/routes/clients/card/card.component.spec.ts b/front/app/src/app/routes/clients/card/card.component.spec.ts new file mode 100644 index 00000000..39952966 --- /dev/null +++ b/front/app/src/app/routes/clients/card/card.component.spec.ts @@ -0,0 +1,25 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClientsCardComponent } from './card.component'; + +describe('ClientsCardComponent', () => { + let component: ClientsCardComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ ClientsCardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClientsCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/front/app/src/app/routes/clients/card/card.component.ts b/front/app/src/app/routes/clients/card/card.component.ts new file mode 100644 index 00000000..337cdd8d --- /dev/null +++ b/front/app/src/app/routes/clients/card/card.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { ClientsService } from '../clients.service' + +export interface Client { + _id: string; + name: string; + address: string; + type: string; +} + +@Component({ + selector: 'app-clients-card', + templateUrl: './card.component.html', + styleUrls: ['./card.component.css'] +}) +export class ClientsCardComponent implements OnInit { + @Input() id: string = ""; + + item: Client = {_id: '', name: '', address: '', type: ''}; + + cardForm = this.formBuilder.group({ + name: '', + address: '', + type: '' + }); + + constructor(private formBuilder: FormBuilder, private clientsService: ClientsService) { + if (this.id == "") { + const url_parts = window.location.href.split('/') + this.id = url_parts[url_parts.length - 1]; + } + this.item = {_id: '', name: '', address: '', type: ''} + } + + ngOnInit() { + this.getData(); + } + + private getData() { + this.clientsService.get(this.id).subscribe((data: any) => { + this.item = data; + }); + } + + onSubmit(): void { + } +} diff --git a/front/app/src/app/routes/clients/clients-routing.module.ts b/front/app/src/app/routes/clients/clients-routing.module.ts new file mode 100644 index 00000000..b97724e9 --- /dev/null +++ b/front/app/src/app/routes/clients/clients-routing.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { ClientsListComponent } from './list/list.component'; +import { ClientsCardComponent } from './card/card.component'; + +const routes: Routes = [{ path: 'list', component: ClientsListComponent }, +{ path: 'card/:id', component: ClientsCardComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ClientsRoutingModule { } diff --git a/front/app/src/app/routes/clients/clients.module.ts b/front/app/src/app/routes/clients/clients.module.ts new file mode 100644 index 00000000..55048782 --- /dev/null +++ b/front/app/src/app/routes/clients/clients.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '@shared/shared.module'; +import { ClientsRoutingModule } from './clients-routing.module'; +import { ClientsListComponent } from './list/list.component'; +import { ClientsCardComponent } from './card/card.component'; + +const COMPONENTS: any[] = [ClientsListComponent, ClientsCardComponent]; +const COMPONENTS_DYNAMIC: any[] = []; + +@NgModule({ + imports: [ + SharedModule, + ClientsRoutingModule + ], + declarations: [ + ...COMPONENTS, + ...COMPONENTS_DYNAMIC + ] +}) +export class ClientsModule { } diff --git a/front/app/src/app/routes/clients/clients.service.ts b/front/app/src/app/routes/clients/clients.service.ts new file mode 100644 index 00000000..8ffd3449 --- /dev/null +++ b/front/app/src/app/routes/clients/clients.service.ts @@ -0,0 +1,23 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import {Client} from "./list/list.component"; + +@Injectable({ + providedIn: 'root' +}) +export class ClientsService { + + constructor(private http: HttpClient) { } + + public getList(page: number, size: number, sortColumn: string, sortDirection: string) { + return this.http.get<{ menu: Client[] }>( + `/api/v1/entity/?size=${size}&page=${page + 1}&sort_by=${sortDirection}(${sortColumn})` + ); + } + + public get(id: string) { + return this.http.get<{ menu: Client[] }>( + `/api/v1/entity/${id}` + ); + } +} diff --git a/front/app/src/app/routes/clients/list/list.component.css b/front/app/src/app/routes/clients/list/list.component.css new file mode 100644 index 00000000..e69de29b diff --git a/front/app/src/app/routes/clients/list/list.component.html b/front/app/src/app/routes/clients/list/list.component.html new file mode 100644 index 00000000..c46e9c99 --- /dev/null +++ b/front/app/src/app/routes/clients/list/list.component.html @@ -0,0 +1,43 @@ + + + + + Filter + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID {{item._id}} Nom {{item.name}} Type {{item.type}} Adresse {{item.address}}
+ + + diff --git a/front/app/src/app/routes/clients/list/list.component.spec.ts b/front/app/src/app/routes/clients/list/list.component.spec.ts new file mode 100644 index 00000000..8d86da69 --- /dev/null +++ b/front/app/src/app/routes/clients/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClientsListComponent } from './list.component'; + +describe('ClientsListComponent', () => { + let component: ClientsListComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ ClientsListComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClientsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/front/app/src/app/routes/clients/list/list.component.ts b/front/app/src/app/routes/clients/list/list.component.ts new file mode 100644 index 00000000..ba52f446 --- /dev/null +++ b/front/app/src/app/routes/clients/list/list.component.ts @@ -0,0 +1,84 @@ +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort, Sort} from '@angular/material/sort'; +import { Injectable } from '@angular/core'; +import { Menu } from "@core"; +import { BehaviorSubject, forkJoin, fromEvent, Observable } from "rxjs"; +import { map, take } from "rxjs/operators"; +import { ClientsService } from '../clients.service' +import {Location} from '@angular/common'; +import { Router } from '@angular/router'; + +export interface Client { + _id: string; + name: string; + address: string; + type: string; +} + +@Component({ + selector: 'app-clients-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.css'] +}) +export class ClientsListComponent implements OnInit { + + displayedColumns: string[] = ['type', 'name', 'address', '_id',]; + dataSource: Client[] = []; + + length: number = 0; + pageIndex: number = 0; + pageSize: number = 15; + + sortColumn: string = "name"; + sortDirection: string = "asc"; + + //@ViewChild(MatSort) sort: MatSort; + + constructor(private location: Location, private clientsService: ClientsService, private router: Router) { } + + ngAfterViewInit() { + } + + ngOnInit() { + this.getData(); + } + + handlePageEvent(e: PageEvent) { + this.length = e.length; + this.pageSize = e.pageSize; + this.pageIndex = e.pageIndex; + this.getData(); + } + + handleSortChange(s: Sort) { + this.sortColumn = s.active; + this.sortDirection = s.direction; + this.getData(); + } + + handleClickRow(row: any) { + window.location.href=window.location.href.replace('list', `card/${row._id}`) + this.router.navigateByUrl("card"); + } + + private getData() { + this.clientsService.getList(this.pageIndex, this.pageSize, this.sortColumn, this.sortDirection).subscribe((data: any) => { + this.dataSource = data.items; + this.length = data.total; + this.pageSize = data.size; + this.pageIndex = data.page - 1; + }); + } + + + + applyFilter(event: Event) { + // const filterValue = (event.target as HTMLInputElement).value; + // this.dataSource.filter = filterValue.trim().toLowerCase(); + // + // if (this.dataSource.paginator) { + // this.dataSource.paginator.firstPage(); + // } + } +} diff --git a/front/app/src/app/routes/routes-routing.module.ts b/front/app/src/app/routes/routes-routing.module.ts index 896ba3aa..f02d18ec 100644 --- a/front/app/src/app/routes/routes-routing.module.ts +++ b/front/app/src/app/routes/routes-routing.module.ts @@ -24,6 +24,7 @@ const routes: Routes = [ { path: '403', component: Error403Component }, { path: '404', component: Error404Component }, { path: '500', component: Error500Component }, + { path: 'clients', loadChildren: () => import('./clients/clients.module').then(m => m.ClientsModule) }, ], }, { diff --git a/front/app/src/assets/data/menu.json b/front/app/src/assets/data/menu.json index 7e1bcf3c..a2bedbe3 100644 --- a/front/app/src/assets/data/menu.json +++ b/front/app/src/assets/data/menu.json @@ -1,5 +1,11 @@ { "menu": [ + { + "route": "clients/list", + "name": "clients", + "type": "link", + "icon": "group" + }, { "route": "dashboard", "name": "dashboard", diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 05c331e8..1cf30cc1 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -34,5 +34,13 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } + + location /ng-cli-ws { + proxy_pass http://docker-front ; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } } } \ No newline at end of file