Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3068f5a2 |
@@ -1,45 +1,59 @@
|
|||||||
# KSK CRM SPA
|
# Kskcrmspa
|
||||||
|
|
||||||
Пет-проект: CRM-система для управления заказами частного мастера.
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.1.5.
|
||||||
Проект создаётся в практических целях для глубинного освоения **Angular 22** и современной экосистемы фронтенд-разработки.
|
|
||||||
|
|
||||||
## Стек технологий
|
## Development server
|
||||||
|
|
||||||
* **Frontend:** Angular 22
|
To start a local development server, run:
|
||||||
* **Backend / Database:** PocketBase
|
|
||||||
* **Package Manager:** pnpm
|
|
||||||
|
|
||||||
## Локальный запуск проекта
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
Проект состоит из двух частей: легковесного бэкенда (PocketBase) и фронтенда (Angular 22).
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
### 1. Запуск бэкенда (PocketBase)
|
## Code scaffolding
|
||||||
|
|
||||||
В репозитории уже лежат файлы структуры базы данных в папке `pb_migrations`. При запуске PocketBase автоматически накатит нужные миграции.
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
1. Скачайте исполняемый файл **PocketBase** для вашей ОС с [официального сайта](https://pocketbase.io/docs/) (или разверните через Docker).
|
```bash
|
||||||
2. Поместите бинарник `pocketbase` в корневую папку этого проекта (на один уровень с папкой `pb_migrations`).
|
ng generate component component-name
|
||||||
3. Запустите сервер:
|
```
|
||||||
```bash
|
|
||||||
./pocketbase serve
|
|
||||||
```
|
|
||||||
*PocketBase запустится на `http://127.0.0.1:8090` и автоматически применит схему БД.*
|
|
||||||
|
|
||||||
### 2. Настройка и запуск фронтенда (Angular)
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
1. Установите зависимости проекта:
|
```bash
|
||||||
```bash
|
ng generate --help
|
||||||
pnpm install
|
```
|
||||||
```
|
|
||||||
2. Убедитесь, что адрес API указан верно. Конфигурация для локальной разработки находится в файле `src/environments/environment.development.ts`:
|
## Building
|
||||||
```typescript
|
|
||||||
export const environment = {
|
To build the project run:
|
||||||
production: false,
|
|
||||||
apiUrl: 'http://127.0.0.1:8090'
|
```bash
|
||||||
};
|
ng build
|
||||||
```
|
```
|
||||||
3. Запустите dev-сервер:
|
|
||||||
```bash
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
pnpm start
|
|
||||||
```
|
## Running unit tests
|
||||||
4. Откройте приложение в браузере по адресу `http://localhost:4200`.
|
|
||||||
|
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ import { provideRouter } from '@angular/router';
|
|||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)],
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideRouter(routes)
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,21 +2,27 @@ import { Routes } from '@angular/router';
|
|||||||
import { TaskList } from './features/tasks/task-list';
|
import { TaskList } from './features/tasks/task-list';
|
||||||
import { TaskDetails } from './features/tasks/task-details';
|
import { TaskDetails } from './features/tasks/task-details';
|
||||||
import { TaskNew } from './features/tasks/task-new';
|
import { TaskNew } from './features/tasks/task-new';
|
||||||
|
import { Dev } from './features/dev';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: TaskList,
|
component: TaskList,
|
||||||
title: 'Главная страница',
|
title: 'Главная страница'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'details/:id',
|
path: 'details/:id',
|
||||||
component: TaskDetails,
|
component: TaskDetails,
|
||||||
title: 'Подробно',
|
title: 'Подробно'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
component: TaskNew,
|
component: TaskNew,
|
||||||
title: 'Новая',
|
title: 'Новая'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'dev',
|
||||||
|
component: Dev,
|
||||||
|
title: 'Dev'
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-2
@@ -21,9 +21,8 @@ import { RouterOutlet } from '@angular/router';
|
|||||||
main {
|
main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
padding: 0.5rem;
|
padding: 1rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [],
|
||||||
|
selector: 'ksk-crm-dev',
|
||||||
|
styles: `
|
||||||
|
.outlined {
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
template: `
|
||||||
|
<p [class.outlined]="isOutlined()" >dev works!</p>
|
||||||
|
<button (click)="handleAction()">Action</button>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class Dev {
|
||||||
|
outlined = signal<string[]>(["rounded"])
|
||||||
|
isOutlined = signal(false);
|
||||||
|
|
||||||
|
handleAction() {
|
||||||
|
this.isOutlined.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,30 +4,18 @@ import { RouterLink } from '@angular/router';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ksk-crm-task-item',
|
selector: 'ksk-crm-task-item',
|
||||||
imports: [RouterLink],
|
imports: [
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
template: `
|
template: `
|
||||||
<a [routerLink]="['/details', task().id]">
|
<p>{{ task().name }}</p>
|
||||||
<p>{{ task().name }}</p>
|
<p>{{ task().city }}, {{ task().streetAddress }}</p>
|
||||||
<p>{{ task().city }}, {{ task().streetAddress }}</p>
|
<p><a [routerLink]="['/details', task().id]">Подробнее...</a></p>
|
||||||
</a>
|
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1rem;
|
margin-bottom: 20px;
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: color-mix(in oklch, var(--color-primary) 25%, transparent);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: color-mix(in oklch, var(--color-primary) 25%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-gray);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { lucideCircleX, lucideSquarePlus } from '@ng-icons/lucide';
|
|||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<input [(ngModel)]="filterInput" type="text" placeholder="Поиск по задачам" />
|
<input [(ngModel)]="filterInput" type="text" placeholder="Поиск по задачам" />
|
||||||
<button (click)="filterInput.set('')">
|
<button (click)="filterInput.set('')">
|
||||||
<ng-icon name="lucideCircleX" size="20" strokeWidth="1.5" />
|
<ng-icon name="lucideCircleX" size="20" strokeWidth="1.5"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a routerLink="/new">
|
<a routerLink="/new">
|
||||||
@@ -24,19 +24,15 @@ import { lucideCircleX, lucideSquarePlus } from '@ng-icons/lucide';
|
|||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
<section class="result">
|
<section class="result">
|
||||||
@for (task of filteredTasks(); track task.id) {
|
@for (task of filteredTasks(); track $index) {
|
||||||
<ksk-crm-task-item [task]="task" />
|
<ksk-crm-task-item [task]="task" />
|
||||||
@if (!$last) {
|
|
||||||
<hr />
|
|
||||||
}
|
|
||||||
} @empty {
|
|
||||||
<p>Список пуст</p>
|
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
.topBar {
|
.topBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -68,23 +64,12 @@ import { lucideCircleX, lucideSquarePlus } from '@ng-icons/lucide';
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 0.5em;
|
margin-left: .5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@@ -92,6 +77,7 @@ export class TaskList {
|
|||||||
taskService: TaskService = inject(TaskService);
|
taskService: TaskService = inject(TaskService);
|
||||||
|
|
||||||
tasks = resource<Task[], null>({
|
tasks = resource<Task[], null>({
|
||||||
|
params: signal(null),
|
||||||
loader: () => this.taskService.getAllTasks(),
|
loader: () => this.taskService.getAllTasks(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,30 +5,32 @@ import { Router } from '@angular/router';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ksk-crm-task-new',
|
selector: 'ksk-crm-task-new',
|
||||||
imports: [ReactiveFormsModule],
|
imports: [
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
template: `
|
template: `
|
||||||
<form [formGroup]="formGroup" (submit)="submitForm()">
|
<form [formGroup]="formGroup" (submit)="submitForm()">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="name">Имя: </label>
|
<label for="name">Имя: </label>
|
||||||
<input id="name" type="text" formControlName="name" />
|
<input id="name" type="text" formControlName="name">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="city">Город: </label>
|
<label for="city">Город: </label>
|
||||||
<input id="city" type="text" formControlName="city" />
|
<input id="city" type="text" formControlName="city">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="streetAddress">Адрес: </label>
|
<label for="streetAddress">Адрес: </label>
|
||||||
<input id="streetAddress" type="text" formControlName="streetAddress" />
|
<input id="streetAddress" type="text" formControlName="streetAddress">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="numberPhone">Номер телефона: </label>
|
<label for="numberPhone">Номер телефона: </label>
|
||||||
<input id="numberPhone" type="text" formControlName="numberPhone" />
|
<input id="numberPhone" type="text" formControlName="numberPhone">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Добавить</button>
|
<button type="submit">Добавить</button>
|
||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
form {
|
form{
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -36,7 +38,7 @@ import { Router } from '@angular/router';
|
|||||||
.form-field {
|
.form-field {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
button {
|
button{
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -50,15 +52,15 @@ export class TaskNew {
|
|||||||
city: new FormControl(''),
|
city: new FormControl(''),
|
||||||
streetAddress: new FormControl(''),
|
streetAddress: new FormControl(''),
|
||||||
numberPhone: new FormControl(''),
|
numberPhone: new FormControl(''),
|
||||||
});
|
})
|
||||||
|
|
||||||
async submitForm() {
|
async submitForm(){
|
||||||
const createdTask = await this.taskService.createTask({
|
const createdTask = await this.taskService.createTask({
|
||||||
name: this.formGroup.value.name ?? '',
|
name: this.formGroup.value.name ?? "",
|
||||||
city: this.formGroup.value.city ?? '',
|
city: this.formGroup.value.city ?? "",
|
||||||
streetAddress: this.formGroup.value.streetAddress ?? '',
|
streetAddress: this.formGroup.value.streetAddress ?? "",
|
||||||
numberPhone: this.formGroup.value.numberPhone ?? '',
|
numberPhone: this.formGroup.value.numberPhone ?? "",
|
||||||
});
|
})
|
||||||
|
|
||||||
this.formGroup.reset();
|
this.formGroup.reset();
|
||||||
await this.router.navigate(['details', createdTask.id]);
|
await this.router.navigate(['details', createdTask.id]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Service } from '@angular/core';
|
import { Service } from '@angular/core';
|
||||||
import { Task } from './task';
|
import { Task } from './task';
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase'
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TaskService {
|
export class TaskService {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'http://localhost:8090',
|
apiUrl: "http://10.1.1.40:8090",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiUrl: 'http://localhost:8090',
|
apiUrl: 'http://10.1.1.40:8090',
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ksk-crm-root></ksk-crm-root>
|
<ksk-crm-root></ksk-crm-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-1
@@ -2,4 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
|
|||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { App } from './app/app';
|
import { App } from './app/app';
|
||||||
|
|
||||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
bootstrapApplication(App, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
|||||||
Reference in New Issue
Block a user