Compare commits

6 Commits
Author SHA1 Message Date
KamaSK b8e7509b79 Merge pull request 'Feature/task list' (#1) from feature/task-list into main
Reviewed-on: #1
2026-09-15 18:14:57 +00:00
KamaSK aa7959e7ac feat(task-item): make card clickable 2026-09-15 21:10:41 +03:00
KamaSK ba6b7e4850 style(task-list): improve view
- align paddings
- convert list to flex
- add divider
2026-09-15 19:45:06 +03:00
KamaSK f81927ee94 fix(environment): change ip pocketbase 2026-09-14 14:49:30 +03:00
KamaSK 5211208297 docs: Обновление README 2026-09-11 17:03:55 +03:00
KamaSK 8f02c4ecce refactor: reformat(prettier) 2026-09-11 17:01:55 +03:00
12 changed files with 100 additions and 93 deletions
+35 -49
View File
@@ -1,59 +1,45 @@
# Kskcrmspa # KSK CRM SPA
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.1.5. Пет-проект: CRM-система для управления заказами частного мастера.
Проект создаётся в практических целях для глубинного освоения **Angular 22** и современной экосистемы фронтенд-разработки.
## Development server ## Стек технологий
To start a local development server, run: * **Frontend:** Angular 22
* **Backend / Database:** PocketBase
* **Package Manager:** pnpm
```bash ## Локальный запуск проекта
ng serve
```
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. Проект состоит из двух частей: легковесного бэкенда (PocketBase) и фронтенда (Angular 22).
## Code scaffolding ### 1. Запуск бэкенда (PocketBase)
Angular CLI includes powerful code scaffolding tools. To generate a new component, run: В репозитории уже лежат файлы структуры базы данных в папке `pb_migrations`. При запуске PocketBase автоматически накатит нужные миграции.
```bash 1. Скачайте исполняемый файл **PocketBase** для вашей ОС с [официального сайта](https://pocketbase.io/docs/) (или разверните через Docker).
ng generate component component-name 2. Поместите бинарник `pocketbase` в корневую папку этого проекта (на один уровень с папкой `pb_migrations`).
``` 3. Запустите сервер:
```bash
./pocketbase serve
```
*PocketBase запустится на `http://127.0.0.1:8090` и автоматически применит схему БД.*
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: ### 2. Настройка и запуск фронтенда (Angular)
```bash 1. Установите зависимости проекта:
ng generate --help ```bash
``` pnpm install
```
## Building 2. Убедитесь, что адрес API указан верно. Конфигурация для локальной разработки находится в файле `src/environments/environment.development.ts`:
```typescript
To build the project run: export const environment = {
production: false,
```bash apiUrl: 'http://127.0.0.1:8090'
ng build };
``` ```
3. Запустите dev-сервер:
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. ```bash
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.
+1 -4
View File
@@ -4,8 +4,5 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)],
provideBrowserGlobalErrorListeners(),
provideRouter(routes)
]
}; };
+4 -4
View File
@@ -7,16 +7,16 @@ 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: 'Новая',
} },
]; ];
+2 -1
View File
@@ -21,8 +21,9 @@ import { RouterOutlet } from '@angular/router';
main { main {
width: 100%; width: 100%;
background: var(--color-surface); background: var(--color-surface);
padding: 1rem; padding: 0.5rem;
border-radius: 5px; border-radius: 5px;
flex-grow: 1;
} }
} }
`, `,
+17 -5
View File
@@ -4,18 +4,30 @@ import { RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'ksk-crm-task-item', selector: 'ksk-crm-task-item',
imports: [ imports: [RouterLink],
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;
margin-bottom: 20px; padding: 1rem;
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);
} }
`, `,
}) })
+19 -5
View File
@@ -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,15 +24,19 @@ import { lucideCircleX, lucideSquarePlus } from '@ng-icons/lucide';
</a> </a>
</section> </section>
<section class="result"> <section class="result">
@for (task of filteredTasks(); track $index) { @for (task of filteredTasks(); track task.id) {
<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;
@@ -64,12 +68,23 @@ 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: .5em; margin-left: 0.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;
} }
`, `,
}) })
@@ -77,7 +92,6 @@ 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(),
}); });
+14 -16
View File
@@ -5,32 +5,30 @@ import { Router } from '@angular/router';
@Component({ @Component({
selector: 'ksk-crm-task-new', selector: 'ksk-crm-task-new',
imports: [ imports: [ReactiveFormsModule],
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;
@@ -38,7 +36,7 @@ import { Router } from '@angular/router';
.form-field { .form-field {
margin-bottom: 10px; margin-bottom: 10px;
} }
button{ button {
padding: 5px; padding: 5px;
} }
`, `,
@@ -52,15 +50,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]);
+2 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
apiUrl: "http://10.1.1.40:8090", apiUrl: 'http://localhost:8090',
}; };
+1 -1
View File
@@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: true, production: true,
apiUrl: 'http://10.1.1.40:8090', apiUrl: 'http://localhost:8090',
}; };
+1 -1
View File
@@ -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>
+1 -2
View File
@@ -2,5 +2,4 @@ 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) bootstrapApplication(App, appConfig).catch((err) => console.error(err));
.catch((err) => console.error(err));