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:
2026-05-23 18:09:44 +03:30
parent 8c07dc7c3f
commit 6ad1a73c16
100 changed files with 768 additions and 231 deletions
@@ -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`
)
)
);
}
}