feat(pos): add shop and statistics modules with components, services, and routing
- Implemented shop module with root component, loading state, and invoice handling. - Created statistics module with invoice type card component, API routes, and data models. - Added season picker component for selecting year and season. - Integrated services for fetching statistics data and managing state. - Established routing for statistics and shop views.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<shared-light-bottomsheet [(visible)]="visible">
|
||||
<div class="mt-1">
|
||||
<label class="mb-1 block text-sm font-medium text-slate-600">انتخاب سال</label>
|
||||
<select
|
||||
[(ngModel)]="selectedYearDraft"
|
||||
class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-700 outline-none">
|
||||
@for (yearItem of years; track yearItem.value) {
|
||||
<option [value]="yearItem.value">{{ yearItem.year }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
@for (seasonItem of seasons; track seasonItem.key) {
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
[label]="seasonItem.label"
|
||||
[severity]="seasonItem.severity"
|
||||
outlined
|
||||
(click)="selectedSeasonDraft.set(seasonItem.key)"></button>
|
||||
}
|
||||
<!-- [disabled]="selectedYear <= selectedYearDraft() && selectedSeason <= selectedSeasonDraft()" -->
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<app-form-footer-actions
|
||||
submitLabel="تایید"
|
||||
cancelLabel="انصراف"
|
||||
(onCancel)="visibleChange.emit(false)"
|
||||
(onSubmit)="onSubmit()" />
|
||||
</div>
|
||||
</shared-light-bottomsheet>
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonDirective, ButtonSeverity } from 'primeng/button';
|
||||
import { SharedLightBottomsheetComponent } from '../dialog/light-bottomsheet.component';
|
||||
import { FormFooterActionsComponent } from '../formFooterActions/form-footer-actions.component';
|
||||
|
||||
type SeasonKey = 0 | 1 | 2 | 3;
|
||||
|
||||
interface SeasonItem {
|
||||
key: SeasonKey;
|
||||
label: string;
|
||||
severity: ButtonSeverity;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'season-picker-dialog',
|
||||
templateUrl: './season-picker-dialog.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
SharedLightBottomsheetComponent,
|
||||
FormFooterActionsComponent,
|
||||
ButtonDirective,
|
||||
],
|
||||
})
|
||||
export class SeasonPickerDialogComponent extends AbstractDialog {
|
||||
@Input() years: { year: number; value: number }[] = [];
|
||||
@Input() selectedYear!: number;
|
||||
@Input() selectedSeason!: SeasonKey;
|
||||
@Input() seasons: SeasonItem[] = [];
|
||||
|
||||
@Output() submit = new EventEmitter<{ year: number; season: number }>();
|
||||
|
||||
selectedYearDraft = signal(this.selectedYear);
|
||||
selectedSeasonDraft = signal(this.selectedSeason);
|
||||
|
||||
isSeasonDisabled = computed(() => this.selectedYear >= this.selectedYearDraft());
|
||||
|
||||
onSubmit(): void {
|
||||
this.submit.emit({
|
||||
year: this.selectedYearDraft(),
|
||||
season: this.selectedSeasonDraft(),
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.selectedYearDraft.set(this.selectedYear);
|
||||
this.selectedSeasonDraft.set(this.selectedSeason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="flex items-center gap-2 overflow-hidden rounded-xl border border-slate-200 bg-white p-2">
|
||||
<p-button icon="pi pi-chevron-right" size="small" text class="shrink-0" (onClick)="prevSeason()" />
|
||||
<div class="flex flex-1 cursor-pointer justify-center" (click)="openPicker()">
|
||||
<span class="text-base font-semibold">{{ currentSeason() }}</span>
|
||||
</div>
|
||||
<p-button icon="pi pi-chevron-left" size="small" text class="shrink-0" (onClick)="nextSeason()" />
|
||||
</div>
|
||||
|
||||
@if (isOpen()) {
|
||||
<season-picker-dialog
|
||||
[visible]="isOpen()"
|
||||
[years]="years()"
|
||||
[seasons]="seasons"
|
||||
[selectedYear]="selectedYear()"
|
||||
[selectedSeason]="selectedSeason()"
|
||||
(visibleChange)="isOpen.set($event)"
|
||||
(submit)="submitPicker($event)" />
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { gregorianToJalali, JALALI_DATE_FORMATS, jalaliToGregorian, nowJalali } from '@/utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, signal } from '@angular/core';
|
||||
import { Button, ButtonSeverity } from 'primeng/button';
|
||||
import { SeasonPickerDialogComponent } from './season-picker-dialog.component';
|
||||
|
||||
type SeasonKey = 0 | 1 | 2 | 3;
|
||||
|
||||
interface SeasonItem {
|
||||
key: SeasonKey;
|
||||
label: string;
|
||||
severity: ButtonSeverity;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'season-picker',
|
||||
templateUrl: 'season-picker.component.html',
|
||||
imports: [CommonModule, Button, SeasonPickerDialogComponent],
|
||||
})
|
||||
export class SeasonPickerComponent implements OnInit {
|
||||
@Input() minYear = 1390;
|
||||
@Input() maxYear = Number(nowJalali(JALALI_DATE_FORMATS.YEAR));
|
||||
@Input() value!: Date;
|
||||
|
||||
@Output() onChange = new EventEmitter<Date>();
|
||||
|
||||
readonly isOpen = signal(false);
|
||||
readonly selectedYear = signal(Number(nowJalali(JALALI_DATE_FORMATS.YEAR)));
|
||||
readonly selectedSeason = signal<SeasonKey>(0);
|
||||
|
||||
readonly seasons: SeasonItem[] = [
|
||||
{ key: 0, label: 'بهار', severity: 'success' },
|
||||
{ key: 1, label: 'تابستان', severity: 'danger' },
|
||||
{ key: 2, label: 'پاییز', severity: 'warn' },
|
||||
{ key: 3, label: 'زمستان', severity: 'info' },
|
||||
];
|
||||
|
||||
readonly years = signal<{ year: number; value: number }[]>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
const year = Number(gregorianToJalali(this.value, JALALI_DATE_FORMATS.YEAR));
|
||||
const monthIndex = Number(gregorianToJalali(this.value, JALALI_DATE_FORMATS.MONTH)) - 1;
|
||||
const season = Math.floor(monthIndex / 3) as SeasonKey;
|
||||
|
||||
this.selectedYear.set(year);
|
||||
this.selectedSeason.set(season);
|
||||
this.years.set(
|
||||
Array.from({ length: this.maxYear - this.minYear + 1 }, (_, i) => ({
|
||||
year: this.minYear + i,
|
||||
value: this.minYear + i,
|
||||
}))
|
||||
);
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
currentSeason(): string {
|
||||
const season = this.seasons.find((item) => item.key === this.selectedSeason());
|
||||
return `${season?.label ?? ''} ${this.selectedYear()}`;
|
||||
}
|
||||
|
||||
openPicker(): void {
|
||||
this.isOpen.set(true);
|
||||
}
|
||||
|
||||
closePicker(): void {
|
||||
this.isOpen.set(false);
|
||||
}
|
||||
|
||||
prevSeason(): void {
|
||||
const index = this.selectedSeason();
|
||||
const currentYear = this.selectedYear();
|
||||
const nextSeason = index === 0 ? 3 : ((index - 1) as SeasonKey);
|
||||
const nextYear = index === 0 ? currentYear - 1 : currentYear;
|
||||
|
||||
// if (!this.isWithinBounds(nextYear, nextSeason)) return;
|
||||
|
||||
this.selectedYear.set(nextYear);
|
||||
this.selectedSeason.set(nextSeason);
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
nextSeason(): void {
|
||||
const index = this.selectedSeason();
|
||||
const currentYear = this.selectedYear();
|
||||
const nextSeason = index === 3 ? 0 : ((index + 1) as SeasonKey);
|
||||
const nextYear = index === 3 ? currentYear + 1 : currentYear;
|
||||
|
||||
this.selectedYear.set(nextYear);
|
||||
this.selectedSeason.set(nextSeason);
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
submitPicker(value: { year: number; season: number }): void {
|
||||
this.selectedYear.set(value.year);
|
||||
this.selectedSeason.set(value.season as SeasonKey);
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
private isWithinBounds(year: number, season: SeasonKey): boolean {
|
||||
if (year < this.minYear || year > this.maxYear) return false;
|
||||
// if (year === this.minYear && season < this.minSeason) return false;
|
||||
// if (year === this.maxYear && season > this.maxSeason) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private emitValue(): void {
|
||||
this.onChange.emit(
|
||||
new Date(
|
||||
jalaliToGregorian(
|
||||
`${this.selectedYear()}/${(this.selectedSeason() * 3 + 1).toString().padStart(2, '0')}/01`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user