| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- import { LessonFeedbackComponent } from './lesson-feedback.component';
- import { lessonDateLabel } from './lesson-display';
- import { DropdownDirective } from '../../shared/dropdown.directive';
- import { CommonModule } from '@angular/common';
- import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, OnInit, OnDestroy, signal, viewChild } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { ActivatedRoute, Router } from '@angular/router';
- import { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, LucideAngularModule, Play, RefreshCw, Save, Star, X } from 'lucide-angular';
- import { finalize, Subscription } from 'rxjs';
- import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
- import { ApiError, ApiService } from '../../core/api.service';
- import { PageMeta } from '../../core/models';
- import { PlatformService } from '../../core/platform.service';
- import { SessionService } from '../../core/session.service';
- import { appointmentDate, beijingDate } from '../../core/appointment-time';
- type SourceItem = Record<string, unknown>;
- @Component({
- selector: 'app-appointment-page',
- standalone: true,
- imports: [LessonFeedbackComponent, DropdownDirective, CommonModule, FormsModule, LucideAngularModule],
- templateUrl: './appointment-page.component.html',
- styleUrl: './appointment-page.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
- })
- export class AppointmentPageComponent implements OnInit, OnDestroy {
- readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
- readonly order = signal<SourceItem>({});
- readonly practiceRecord = signal<SourceItem>({});
- readonly learningRecord = signal<SourceItem>({});
- readonly courses = signal<SourceItem[]>([]);
- readonly coursesLoading = signal(false);
- readonly coursesError = signal('');
- readonly unitsLoading = signal(false);
- private courseRequest?: Subscription;
- private unitRequest?: Subscription;
- readonly units = signal<SourceItem[]>([]);
- readonly selectedUnits = signal<Set<string>>(new Set());
- readonly completedUnits = signal<Set<string>>(new Set());
- readonly bindingRecord = signal<SourceItem>({});
- readonly loading = signal(true);
- readonly submitting = signal(false);
- readonly error = signal('');
- readonly detailWarning = signal('');
- readonly feedbackError = signal('');
- readonly editingFeedback = signal(false);
- readonly feedbackSaved = signal(false);
- private feedbackRevision = '';
- private autoEditOpened = false;
- readonly dateLabel = lessonDateLabel;
- readonly actionError = signal('');
- readonly detailVerified = signal(false);
- private loadRequest?: Subscription;
- private stopLive?: () => void;
- private readonly liveData = inject(LiveDataRefreshService);
- readonly completionDialog = viewChild<ElementRef<HTMLDialogElement>>('completionDialog');
- readonly icons = { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, Play, RefreshCw, Save, Star, X };
- readonly status = computed(() => Number(this.order()['dszt'] ?? this.order()['status'] ?? 0));
- readonly feedback=computed(()=>this.order()['feedback'] as Record<string,unknown>|null);
- selectedCourse = '';
- selectedBindingId = '';
- coachComment = '';
- contentSummary = '';
- reviewStatus = '';
- reviewSummary = '';
- trainingStatus = '';
- trainingSummary = '';
- homework = '';
- noHomework = false;
- notes = '';
- studentScore = 5;
- studentComment = '';
- private readonly route = inject(ActivatedRoute);
- private readonly router = inject(Router);
- private readonly api = inject(ApiService);
- private readonly session = inject(SessionService);
- private readonly platform = inject(PlatformService);
- constructor() {
- effect(() => {
- const dialog = this.completionDialog()?.nativeElement;
- if (dialog && !this.autoEditOpened && this.can('editFeedback') && this.feedback() && this.route.snapshot.queryParamMap.get('editReport') === '1') { this.autoEditOpened = true; this.openFeedbackEditor(); }
- if (dialog && !this.isStudentView && !this.order()['cancelled'] && this.can('complete') && this.route.snapshot.queryParamMap.get('feedback') === '1') dialog.showModal();
- }, { allowSignalWrites: true });
- }
- ngOnInit(): void {
- this.load();
- this.stopLive = this.liveData.watch({scopes:['appointments'],studentId:()=>undefined,appointmentPolling:true,onRefresh:()=>{if(!this.submitting() && !this.completionDialog()?.nativeElement.open)this.load(true);}});
- }
- ngOnDestroy(): void { this.stopLive?.(); this.loadRequest?.unsubscribe(); this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe(); }
- can(action: string): boolean { return this.detailVerified() && !this.order()['cancelled'] && Array.isArray(this.order()['allowedActions']) && (this.order()['allowedActions'] as string[]).includes(action); }
- openReport(): void { void this.router.navigate(['/pages/shop/baobiao'], {queryParams:{id:this.orderId}}); }
- get isStudentView(): boolean { return this.meta.path === 'pages/shop/stu_get_order'; }
- get orderId(): string { return this.route.snapshot.queryParamMap.get('id') ?? this.route.snapshot.queryParamMap.get('cart') ?? ''; }
- get selectedCourseItem(): SourceItem | undefined { return this.courses().find(row => String(row['bindingId']) === this.selectedBindingId); }
- get isWriting(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.selectedCourseItem?.['categoryKey'] || this.order()['courseCategoryKey'] || '')); }
- load(background = false): void {
- this.loadRequest?.unsubscribe();
- if (!this.orderId) {
- this.loading.set(false);
- this.error.set('缺少预约编号');
- return;
- }
- if (!background) { this.loading.set(true); this.detailVerified.set(false); }
- this.error.set('');
- this.detailWarning.set('');
- const fallback = this.appointmentSnapshot();
- this.loadRequest = this.api.get<unknown>('e_order_detail', { id: this.orderId }).pipe(finalize(() => this.loading.set(false))).subscribe({
- next: ({ result }) => {
- const detail = this.findList(result)[0] ?? this.asRecord(result);
- this.detailVerified.set(String(detail['GeneralID'] ?? '') === this.orderId);
- if (!this.detailVerified()) this.detailWarning.set('未能核实预约详情,请刷新后重试。当前课程仅供查看。');
- // Older servers already support starting lessons. Missing new workflow metadata
- // must not disable that endpoint; never derive write access from the cached row.
- if (!Array.isArray(detail['allowedActions'])) Object.assign(detail, this.legacyStartActions(detail));
- if (background) { const changed = this.order()['bindingId'] !== detail['bindingId']; this.order.set({...this.order(),...detail}); if (changed && !this.isStudentView) this.loadCourses(); return; }
- this.applyOrder({ ...fallback, ...detail });
- },
- error: (error: ApiError) => {
- this.detailVerified.set(false);
- if (background) { this.detailWarning.set('详情更新失败,已暂停操作,请刷新后重试:'+error.message); return; }
- if (Object.keys(fallback).length) {
- this.applyOrder(fallback);
- this.detailWarning.set('详情接口暂时返回异常,当前显示排课列表里的课程信息。你可以返回列表后重试。');
- this.loading.set(false);
- return;
- }
- this.error.set(this.friendlyDetailError(error.message));
- },
- });
- }
- private legacyStartActions(detail: SourceItem): SourceItem {
- const result: SourceItem = { allowedActions: [], blockedReason: '' };
- if (!this.detailVerified() || this.isStudentView || detail['cancelled']) return result;
- const teacherId = Number(detail['pl']);
- if (!teacherId || teacherId !== this.session.user()?.userId) {
- return { ...result, blockedReason: '仅本节课的老师可以开始或继续课程' };
- }
- const status = Number(detail['dszt'] ?? detail['status']);
- if (status === 10) return { ...result, allowedActions: ['continue'] };
- if (status !== 0) return result;
- const date = appointmentDate(detail);
- if (!date) return { ...result, blockedReason: '预约日期不完整,请联系管理员核对排课' };
- if (date !== beijingDate()) return { ...result, blockedReason: '课程仅在预约当天(北京时间)可开始,其他日期请联系管理员改期' };
- return { ...result, allowedActions: ['start'] };
- }
- field(label: string): string {
- const aliases: Record<string, string[]> = {
- student: ['HoneyName', 'honeyName', 'honeyname', 'xymz_t'],
- course: ['yykcmc', 'kcmc', 'Subtitle', 'ctitle', 'Title'],
- type: ['dslx', 'dslx_t', 'kclx'],
- date: ['bxrq', 'yydate'],
- time: ['sdsd', 'startTime'],
- end: ['jieshu', 'endTime'],
- delivery: ['jffs'],
- coach: ['plxm', 'plName'],
- };
- if (label === 'type') return String(this.order()['courseCategoryName'] || ({1:'30分钟课',2:'60分钟课',3:'体验课',4:'课型4'} as Record<string,string>)[String(this.order()['dslx'])] || this.order()['dslx_t'] || '课程');
- for (const key of aliases[label] ?? []) if (this.order()[key] !== undefined) return String(this.order()[key]);
- return '';
- }
- statusText(): string {
- if(this.order()['cancelled'])return '已取消';
- return String(this.order()['status_txt'] ?? ({ 0: '待开始', 10: '学习中', 11: '等待学员确认', 20: '学员已确认', 30: '已完成', 40: '已完成' } as Record<number, string>)[this.status()] ?? `状态 ${this.status()}`);
- }
- private applyOrder(order: SourceItem): void {
- this.order.set(order);
- this.syncAppointmentStudent(order);
- if (!this.selectedBindingId) { this.selectedCourse = String(order['kcid'] ?? ''); this.selectedBindingId = String(order['bindingId'] || ''); }
- if (!this.coachComment) this.coachComment = String(order['pldp'] ?? '');
- this.loadPracticeRecord();
- if (!this.isStudentView) {
- if (this.status() <= 10) this.loadCourses();
- this.loadLearningRecord();
- }
- }
- chooseCourse(): void {
- this.unitRequest?.unsubscribe();
- this.units.set([]); this.selectedUnits.set(new Set()); this.completedUnits.set(new Set()); this.bindingRecord.set({});
- this.actionError.set('');
- const course = this.selectedCourseItem;
- this.selectedCourse = String(course?.['kcid'] || '');
- if (!course) return;
- this.bindingRecord.set(course);
- this.completedUnits.set(new Set(Object.keys(this.parseRecord(course['syjd']))));
- if (!this.isWriting) this.loadUnits();
- }
- toggleUnit(unit: SourceItem): void {
- const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
- this.selectedUnits.update((current) => {
- const next = new Set(current);
- if (next.has(id)) next.delete(id); else next.add(id);
- return next;
- });
- }
- async markUnitComplete(unit: SourceItem): Promise<void> {
- if (!this.detailVerified() || (!this.can('start') && !this.can('continue'))) return;
- const selectedBindingId = this.selectedBindingId;
- if (!(await this.platform.confirm(`确认将“${this.unitTitle(unit)}”标记为已学完?`))) return;
- if (selectedBindingId !== this.selectedBindingId) return;
- const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
- const binding = this.bindingRecord();
- if (!binding['GeneralID']) {
- this.platform.toast('未找到课程进度记录', 'error');
- return;
- }
- const progress = this.parseRecord(binding['syjd']);
- progress[id] = `${id}@${this.unitTitle(unit)}@1`;
- this.api.post('content_update', {
- content: JSON.stringify({ GeneralID: binding['GeneralID'], ModelID: 58 }),
- addon: JSON.stringify({ syjd: JSON.stringify(progress) }),
- }).subscribe({
- next: () => { this.courses.update(rows => rows.map(row => String(row['bindingId']) === selectedBindingId ? {...row,syjd:JSON.stringify(progress)} : row)); if (selectedBindingId === this.selectedBindingId) { this.bindingRecord.set({...binding,syjd:JSON.stringify(progress)}); this.completedUnits.update(current => new Set(current).add(id)); } },
- error: (error: ApiError) => this.platform.toast(error.message, 'error'),
- });
- }
- unitTitle(unit: SourceItem): string {
- return String(unit['NodeName'] ?? unit['Title'] ?? '学习单元');
- }
- unitId(unit: SourceItem): string {
- return String(unit['NodeID'] ?? unit['ID'] ?? '');
- }
- unitProgress(unit: SourceItem): { learned: number; total: number; percent: number } | null {
- const rawTotal = unit['total'];
- const rawLearned = unit['yx_word'];
- if (rawTotal == null || rawTotal === '' || rawLearned == null || rawLearned === '') return null;
- const total = Number(rawTotal);
- const learned = Number(rawLearned);
- if (!Number.isFinite(total) || !Number.isFinite(learned) || total < 0 || learned < 0) return null;
- return {
- learned: Math.min(learned, total),
- total,
- percent: total > 0 ? Math.min(100, Math.round(learned / total * 100)) : 0,
- };
- }
- startOrContinue(): void {
- this.actionError.set('');
- if (!this.can('start') && !this.can('continue')) { this.actionError.set(String(this.order()['blockedReason'] || '详情未核实,请刷新后重试')); return; }
- if(this.order()['cancelled']===true){this.platform.toast('此预约已取消,不能开始课程','error');return;}
- if (this.coursesLoading() || this.unitsLoading() || !this.selectedCourseItem || this.coursesError()) { this.actionError.set('请先加载并选择该学员已开通的课程'); return; }
- if (!this.isWriting && !this.selectedUnits().size) {
- this.actionError.set(this.units().length ? '请选择本次学习单元' : '暂无可用学习单元,请刷新重试或联系管理员核对课程内容');
- return;
- }
- this.updateOrderStatus(10, () => {
- if (this.isWriting) {
- this.load();
- return;
- }
- void this.router.navigate(['/pages/stu/stu_abc'], {
- replaceUrl: true,
- queryParams: { nids: [...this.selectedUnits()].join(','), pnid: this.selectedCourse, orderId: this.orderId, bindingId:this.selectedBindingId||undefined },
- });
- });
- }
- openCompletionFeedback(): void {
- if(this.order()['cancelled']===true)return;
- if (this.isStudentView || !this.can('complete') || this.submitting()) return;
- this.editingFeedback.set(false);
- this.feedbackError.set('');
- this.completionDialog()?.nativeElement.showModal();
- }
- openFeedbackEditor(): void {
- if (this.isStudentView || !this.can('editFeedback') || !this.feedback() || this.submitting()) return;
- const feedback = this.feedback()!;
- this.contentSummary = String(feedback['contentSummary'] || '');
- this.reviewStatus = String(feedback['reviewStatus'] || '');
- this.reviewSummary = String(feedback['reviewSummary'] || '');
- this.trainingStatus = String(feedback['trainingStatus'] || '');
- this.trainingSummary = String(feedback['trainingSummary'] || '');
- this.homework = String(feedback['homework'] || '');
- this.noHomework = feedback['noHomework'] === true;
- this.coachComment = String(feedback['coachComment'] ?? this.practiceRecord()['pldp'] ?? '');
- this.notes = String(feedback['notes'] || '');
- this.feedbackRevision = String(feedback['revision'] || feedback['updatedAt'] || '');
- this.editingFeedback.set(true);
- this.feedbackError.set('');
- this.feedbackSaved.set(false);
- this.completionDialog()?.nativeElement.showModal();
- }
- closeCompletionFeedback(event?: Event): void {
- event?.preventDefault();
- if (!this.submitting()) this.completionDialog()?.nativeElement.close();
- }
- finishCoachLesson(): void {
- if (this.submitting() || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete') || !this.completionDialog()?.nativeElement.open) return;
- this.feedbackError.set('');
- if (!this.contentSummary.trim()) { this.feedbackError.set('请填写本次学习内容'); return; }
- if (!this.trainingSummary.trim()) { this.feedbackError.set('请填写进度说明'); return; }
- if (!this.noHomework && !this.homework.trim()) { this.feedbackError.set('请填写作业安排,或选择“无作业”'); return; }
- this.submitting.set(true);
- const editing = this.editingFeedback();
- this.api.post(editing ? 'app_lesson_feedback_update' : 'app_lesson_complete', {
- orderId: this.orderId,
- expectedUpdatedAt: editing ? this.feedbackRevision : this.order()['updatedAt'],
- feedback: JSON.stringify({
- contentSummary: this.contentSummary.trim(),
- reviewStatus: this.reviewStatus.trim(),
- reviewSummary: this.reviewSummary.trim(),
- trainingStatus: this.trainingStatus.trim(),
- trainingSummary: this.trainingSummary.trim(),
- homework: this.noHomework ? '无作业' : this.homework.trim(),
- noHomework: this.noHomework,
- notes: this.notes.trim(),
- coachComment: this.coachComment.trim(),
- }),
- }).pipe(finalize(() => this.submitting.set(false))).subscribe({
- next: () => { this.completionDialog()?.nativeElement.close(); this.feedbackSaved.set(editing); this.platform.toast(editing ? '报告修改已保存' : '课程已结课,学习报告已生成'); this.load(); },
- error: (error: ApiError) => this.feedbackError.set(error.message),
- });
- }
- finishStudentLesson(): void {
- if(this.submitting() || !this.can('review')) return;
- this.actionError.set('');
- if (!Number.isInteger(this.studentScore) || this.studentScore < 1 || this.studentScore > 5) { this.actionError.set('评分应为1至5分'); return; }
- this.submitting.set(true);
- this.api.post('app_lesson_review',{orderId:this.orderId,score:this.studentScore,comment:this.studentComment.trim()}).pipe(finalize(()=>this.submitting.set(false))).subscribe({
- next:()=>{this.platform.toast('评价已提交');this.load();}, error:(error:ApiError)=>this.actionError.set(error.message),
- });
- }
- private updateOrderStatus(status:number,next:()=>void):void{
- if(this.submitting())return;this.submitting.set(true);
- this.api.post('e_order_update_v2',{status,uid:this.session.user()?.userId,learningBindingId:this.selectedBindingId,expectedUpdatedAt:this.order()['updatedAt'],content:JSON.stringify({GeneralID:this.orderId})}).pipe(finalize(()=>this.submitting.set(false))).subscribe({next:()=>next(),error:(error:ApiError)=>this.actionError.set(error.message)});
- }
- private loadPracticeRecord(): void {
- const user = this.session.user();
- const targetField = this.isStudentView ? `yyds=${this.orderId}|xymz=${user?.userId}` : `yyds=${this.orderId}|jsmz=${user?.userId}`;
- this.api.get<unknown>('content_list', { ModelID: 59, psize: 2, myfield2: targetField }).subscribe({
- next: ({ result }) => {
- const records=this.findList(result),bindingId=String(this.order()['bindingId']||'');
- const record = records.find(row=>String(row['yyds']||'')===this.orderId)|| (records.length===1?records[0]:undefined) || {};
- this.practiceRecord.set(record);
- if (!this.coachComment) this.coachComment = String(record['pldp'] ?? '');
- if (Number(record['pf']) > 0) { this.studentScore = Number(record['pf']); this.studentComment = String(record['pjnr'] ?? ''); }
- },
- error: () => undefined,
- });
- }
- private loadCourses(): void {
- this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe();
- this.coursesLoading.set(true); this.coursesError.set(''); this.units.set([]); this.selectedUnits.set(new Set());
- this.courseRequest = this.api.get<unknown>('app_lesson_courses', { orderId: this.orderId }).pipe(finalize(() => this.coursesLoading.set(false))).subscribe({
- next: ({result}) => {
- const courses = this.findList(result);
- this.courses.set(courses);
- const selected = courses.find(row => String(row['bindingId']) === this.selectedBindingId)
- || courses.find(row => String(row['bindingId']) === String(this.order()['bindingId'] || ''));
- this.selectedBindingId = String(selected?.['bindingId'] || '');
- this.chooseCourse();
- },
- error: (error: ApiError) => { this.courses.set([]); this.selectedBindingId = ''; this.coursesError.set('学员课程加载失败:' + error.message); },
- });
- }
- private loadUnits(): void {
- const studentId = this.appointmentStudentId(), bindingId = this.selectedBindingId;
- if (!studentId || !bindingId) return;
- this.unitsLoading.set(true);
- this.unitRequest = this.api.get<unknown>('node_list', {pid:this.selectedCourse,bindingId,ifunit:1,userId:studentId,psize:1000}).pipe(finalize(() => this.unitsLoading.set(false))).subscribe({
- next: ({result}) => { if (bindingId === this.selectedBindingId) this.units.set(this.findList(result)); },
- error: (error: ApiError) => { if (bindingId === this.selectedBindingId) this.actionError.set('学习单元加载失败:' + error.message); },
- });
- }
- private loadLearningRecord(): void {
- const studentId = this.appointmentStudentId();
- if (!studentId) return;
- this.api.get<unknown>('content_list', { ModelID: 56, psize: 1, myfield: `dsid=${this.orderId}`, myfield2: `UserId=${studentId}|pl=${this.session.user()?.userId}` }).subscribe({
- next: ({ result }) => this.learningRecord.set(this.findList(result)[0] ?? {}),
- error: () => undefined,
- });
- }
- private parseRecord(value: unknown): Record<string, string> {
- if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, string>;
- if (typeof value !== 'string' || !value) return {};
- try { return JSON.parse(value) as Record<string, string>; } catch { return {}; }
- }
- private findList(value: unknown): SourceItem[] {
- if (Array.isArray(value)) return value as SourceItem[];
- if (value && typeof value === 'object') for (const child of Object.values(value)) if (Array.isArray(child)) return child as SourceItem[];
- return [];
- }
- private asRecord(value: unknown): SourceItem {
- return value && typeof value === 'object' && !Array.isArray(value) ? value as SourceItem : {};
- }
- private appointmentStudentId(): number {
- return Number(this.order()['szyh'] ?? this.order()['yhid'] ?? this.session.student()?.userId ?? 0);
- }
- private syncAppointmentStudent(order: SourceItem): void {
- const studentId = Number(order['szyh'] ?? order['yhid'] ?? 0);
- if (!studentId || this.session.student()?.userId === studentId) return;
- this.session.setStudent({
- userId: studentId,
- honeyName: String(order['HoneyName'] ?? order['honeyName'] ?? order['honeyname'] ?? order['xymz_t'] ?? '学员'),
- groupId: 1,
- });
- }
- private appointmentSnapshot(): SourceItem {
- const state = history.state as { appointmentSnapshot?: unknown } | null;
- if (state?.appointmentSnapshot && typeof state.appointmentSnapshot === 'object' && !Array.isArray(state.appointmentSnapshot)) {
- return state.appointmentSnapshot as SourceItem;
- }
- try {
- const stored = sessionStorage.getItem(`xiaoshu.appointment.snapshot.${this.orderId}`);
- const parsed = stored ? JSON.parse(stored) as unknown : null;
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as SourceItem : {};
- } catch {
- return {};
- }
- }
- private friendlyDetailError(message: string): string {
- return message.includes('Object reference not set to an instance of an object')
- ? '课程详情暂时无法加载,请返回排课列表重试。'
- : message;
- }
- private dateOnly(): string {
- const now = new Date();
- return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
- }
- private dateTime(): string {
- const now = new Date();
- return `${this.dateOnly()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
- }
- }
|