This commit is contained in:
Chris Chen 2024-02-28 15:17:41 -08:00
parent 42e7ee39be
commit 6301d6008b
124 changed files with 2655 additions and 667 deletions

View File

@ -10,7 +10,7 @@ import {
LayoutService,
PlayerService,
SeoService,
StateService,
StateServiceForNB,
} from './utils';
import { UserData } from './data/users';
import { ElectricityData } from './data/electricity';
@ -142,7 +142,7 @@ export const NB_CORE_PROVIDERS = [
LayoutService,
PlayerService,
SeoService,
StateService,
StateServiceForNB,
];
@NgModule({

View File

@ -1,7 +1,7 @@
import { LayoutService } from './layout.service';
import { AnalyticsService } from './analytics.service';
import { PlayerService } from './player.service';
import { StateService } from './state.service';
import { StateServiceForNB } from './state.service';
import { SeoService } from './seo.service';
export {
@ -9,5 +9,5 @@ export {
AnalyticsService,
PlayerService,
SeoService,
StateService,
StateServiceForNB,
};

View File

@ -1,11 +1,11 @@
import { Injectable, OnDestroy } from '@angular/core';
import { of as observableOf, Observable, BehaviorSubject } from 'rxjs';
import { of as observableOf, Observable, BehaviorSubject } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { NbLayoutDirectionService, NbLayoutDirection } from '@nebular/theme';
@Injectable()
export class StateService implements OnDestroy {
export class StateServiceForNB implements OnDestroy {
protected layouts: any = [
{
@ -58,7 +58,7 @@ export class StateService implements OnDestroy {
}
private updateSidebarIcons(direction: NbLayoutDirection) {
const [ startSidebar, endSidebar ] = this.sidebars;
const [startSidebar, endSidebar] = this.sidebars;
const isLtr = direction === NbLayoutDirection.LTR;
const startIconClass = isLtr ? 'nb-layout-sidebar-left' : 'nb-layout-sidebar-right';
const endIconClass = isLtr ? 'nb-layout-sidebar-right' : 'nb-layout-sidebar-left';

View File

@ -0,0 +1,34 @@
export const DEFAULT_MEDIA_BREAKPOINTS = [
{
name: 'xs',
width: 0,
},
{
name: 'is',
width: 400,
},
{
name: 'sm',
width: 576,
},
{
name: 'md',
width: 768,
},
{
name: 'lg',
width: 992,
},
{
name: 'xl',
width: 1200,
},
{
name: 'xxl',
width: 1400,
},
{
name: 'xxxl',
width: 1600,
},
];

View File

@ -9,6 +9,7 @@ import { HeaderService } from '../../../services/header.service';
import { NbAuthService } from '@nebular/auth';
import { AuthService } from '../../../services/auth.service';
import { UserProfileAction } from '../../../entity/Auth';
import { Router } from '@angular/router';
@Component({
selector: 'ngx-header',
@ -19,6 +20,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
header: string = '';
private destroy$: Subject<void> = new Subject<void>();
userPictureOnly: boolean = false;
isLessThanMd: boolean = false;
themes = [
{
@ -68,7 +70,8 @@ export class HeaderComponent implements OnInit, OnDestroy {
return null;
}
}
constructor(private sidebarService: NbSidebarService,
constructor(
private sidebarService: NbSidebarService,
private menuService: NbMenuService,
private themeService: NbThemeService,
private userService: UserData,
@ -77,15 +80,12 @@ export class HeaderComponent implements OnInit, OnDestroy {
private headerService: HeaderService,
private oAuthService: NbAuthService,
private authService: AuthService,
protected router: Router,
) {
this.headerService.headerChange$.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.header = result;
});
this.menuService.onItemClick().pipe(takeUntil(this.destroy$))
.subscribe(result => {
if (result.item.title == 'Log out') this.logout();
});
}
@ -93,13 +93,18 @@ export class HeaderComponent implements OnInit, OnDestroy {
ngOnInit() {
this.currentTheme = this.themeService.currentTheme;
const { xl } = this.breakpointService.getBreakpointsMap();
const { md, xl } = this.breakpointService.getBreakpointsMap();
this.themeService.onMediaQueryChange()
.pipe(
map(([, currentBreakpoint]) => currentBreakpoint.width < xl),
map(([, currentBreakpoint]) => currentBreakpoint.width),
takeUntil(this.destroy$),
)
.subscribe((isLessThanXl: boolean) => this.userPictureOnly = isLessThanXl);
.subscribe((screenWidth: number) => {
let isLessThanXl = screenWidth < xl;
this.isLessThanMd = screenWidth < md;
this.userPictureOnly = isLessThanXl
});
this.themeService.onThemeChange()
.pipe(
@ -108,6 +113,22 @@ export class HeaderComponent implements OnInit, OnDestroy {
)
.subscribe(themeName => this.currentTheme = themeName);
this.menuService.onItemClick().subscribe(result => {
if (this.isLessThanMd && result.tag == 'NavMenu' && result.item.link) {
this.toggleSidebar();
} else if (result.tag == 'UserProfileMenu') {
switch (result.item.data as UserProfileAction) {
case UserProfileAction.None: break;
case UserProfileAction.GoToProfile:
this.router.navigate(["/myapp/profile"]);
break;
case UserProfileAction.LogOut:
this.logout();
break;
default: break;
}
}
});
}
ngOnDestroy() {

View File

@ -1,4 +1,4 @@
<ngx-one-column-layout>
<nb-menu [items]="MENU_ITEMS"></nb-menu>
<nb-menu [items]="MENU_ITEMS" tag="NavMenu"></nb-menu>
<router-outlet></router-outlet>
</ngx-one-column-layout>

View File

@ -6,10 +6,7 @@ import { AdminComponent } from './admin.component';
import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbSpinnerModule, NbDialogModule, NbUserModule } from '@nebular/theme';
import { ThemeModule } from '../@theme/theme.module';
import { FormsModule } from '@angular/forms';
import { BestListDlgComponent } from './happiness-groups/best-list-dlg/best-list-dlg.component';
import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module';
import { HappinessWeekEditorComponent } from './happiness-groups/happiness-week-editor/happiness-week-editor.component';
import { HappinessWeekListDlgComponent } from './happiness-groups/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { CellGroupRoutineEventsComponent } from './cell-group-routine-events/cell-group-routine-events.component';
import { FancyTableModule } from '../ui/fancy-table/fancy-table.module';
import { FamilyMembersComponent } from './family-members/family-members.component';
@ -26,14 +23,9 @@ import { MaskDirectiveModule } from '../directives/mask-directive/mask-directive
import { DateInputModule } from '../ui/date-input/date-input.module';
import { LineMessagingAccountComponent } from './lines/line-messaging-account/line-messaging-account.component';
import { LineMessagingAccountEditorComponent } from './lines/line-messaging-account-editor/line-messaging-account-editor.component';
import { WeekTaskEditorComponent } from './happiness-groups/week-task-editor/week-task-editor.component';
@NgModule({
declarations: [
AdminComponent,
BestListDlgComponent,
HappinessWeekEditorComponent,
HappinessWeekListDlgComponent,
CellGroupRoutineEventsComponent,
FamilyMembersComponent,
FamilyMemberEditorComponent,
@ -44,7 +36,7 @@ import { WeekTaskEditorComponent } from './happiness-groups/week-task-editor/wee
LogDetailComponent,
LineMessagingAccountComponent,
LineMessagingAccountEditorComponent,
WeekTaskEditorComponent],
],
imports: [
CommonModule,
FormsModule,

View File

@ -4,10 +4,10 @@ import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { inherits } from 'util';
import { StateService } from '../../@core/utils';
import { CellGroupRoutineEvents } from '../../entity/CellGroupRoutineEvents';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';
import { ObjectUtils } from '../../utilities/object-utils';

View File

@ -3,10 +3,10 @@ import { NbDialogRef } from '@nebular/theme';
import { DropDownOption } from '../../../entity/dropDownOption';
import { DomainMemberRelationship, PastoralDomain } from '../../../entity/PastoralDomain';
import { DialogComponent } from '../../../pages/modal-overlays/dialog/dialog.component';
import { DomainMemberShipService } from '../../../services/crudServices/pastoral-domain.service';
import { ArrayUtils } from '../../../utilities/array-utils';
import { first } from "rxjs/operators"
import { FamilyMember } from '../../../entity/Member';
import { DomainMemberShipService } from '../../../services/crudServices/domain-member-ship.service';
@Component({
selector: 'ngx-assign-member-cell-group',

View File

@ -3,12 +3,12 @@ import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { StateService } from '../../@core/utils';
import { FamilyMember, Gender } from '../../entity/Member';
import { PastoralDomain } from '../../entity/PastoralDomain';
import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';
import { ObjectUtils } from '../../utilities/object-utils';

View File

@ -1,4 +0,0 @@
nb-card {
max-height: 90vh;
width: 700px;
}

View File

@ -1,84 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef, NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { HappinessBEST } from '../../../entity/HappinessGroup';
import { PastoralDomain } from '../../../entity/PastoralDomain';
import { BestService } from '../../../services/crudServices/best.service';
import { HappinessGroupService } from '../../../services/crudServices/happiness-group.service';
import { LineGroup, LineService } from '../../../services/line.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { ADButtons, AlertDlgComponent } from '../../../ui/alert-dlg/alert-dlg.component';
@Component({
selector: 'ngx-best-list-dlg',
templateUrl: './best-list-dlg.component.html',
styleUrls: ['./best-list-dlg.component.scss']
})
export class BestListDlgComponent implements OnInit {
processing: boolean = false;
isAdding: boolean = false;
group: PastoralDomain;
datum: HappinessBEST;
constructor(
private dlgRef: NbDialogRef<BestListDlgComponent>,
private msgBpxService: MsgBoxService,
private happinessGroupService: HappinessGroupService,
private toastrService: NbToastrService,
private bestService: BestService,
private lineService: LineService
) { }
ngOnInit(): void {
this.group.bests = this.group.bests.sort((a, b) => 0 - (a.name < b.name ? 1 : -1));
}
close() {
this.dlgRef.close();
}
update() {
this.dlgRef.close(this.datum);
}
addBest() {
this.msgBpxService.showInputbox('Add Best', 'Please input Best\'s Name').pipe(first()).subscribe(result => {
if (result) {
let obj = { groupId: this.group.id, id: '', name: result } as HappinessBEST;
this.bestService.createOrUpdate(obj).pipe(first()).subscribe(result => {
this.dlgRef.close();
});
}
});
}
copyBestUrl(best: HappinessBEST) {
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = `${environment.invitationUrl}${best.id}`;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
}
copyBestList() {
let list = "".concat(...this.group.bests.map(b => `${b.name}\t\t: ${environment.invitationUrl}${b.id}\r\n`));
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = list;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
this.lineService.pushLineMessage(LineGroup.Chris, list);
}
downloadBestQRCode(best: HappinessBEST) {
this.happinessGroupService.getBestQrCode(best.name + ".png", best.id);
}
}

View File

@ -1,4 +0,0 @@
nb-card {
max-height: 90vh;
width: 700px;
}

View File

@ -1,52 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef, NbDialogService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { HappinessWeek } from '../../../entity/HappinessGroup';
import { PastoralDomain } from '../../../entity/PastoralDomain';
import { NumberUtils } from '../../../utilities/number-utils';
import { HappinessWeekEditorComponent } from '../happiness-week-editor/happiness-week-editor.component';
import { WeekTaskEditorComponent } from '../week-task-editor/week-task-editor.component';
@Component({
selector: 'ngx-happiness-week-list-dlg',
templateUrl: './happiness-week-list-dlg.component.html',
styleUrls: ['./happiness-week-list-dlg.component.scss']
})
export class HappinessWeekListDlgComponent implements OnInit {
group: PastoralDomain;
constructor(
private dlgRef: NbDialogRef<any>,
private dlgService: NbDialogService
) { }
ngOnInit(): void {
}
close() {
this.dlgRef.close();
}
openEditDlg(week: HappinessWeek) {
this.dlgService.open(HappinessWeekEditorComponent, {
context: {
data: week
}
}).onClose.pipe(first()).subscribe(result => {
if (result) this.close();
});
}
openTaskDlg(week: HappinessWeek) {
this.dlgService.open(WeekTaskEditorComponent, {
context: {
data: week,
allData: this.group.happinessWeeks
}
}).onClose.pipe(first()).subscribe(result => {
});
}
weekDisplay(seq: number) {
return `W${seq} ${this.group.happinessWeeks[seq - 1].topic}`;
}
}

View File

@ -1,38 +0,0 @@
<form #form="ngForm">
<nb-card>
<nb-card-header>
第 {{data.seq}} 周 {{data.topic}} 分工
</nb-card-header>
<nb-card-body *ngIf="data">
<div class="row" *ngFor="let task of data.tasks;let i = index">
<div class="col-md-4">
<div class='form-group'>
<label for='tasker{{i}}' class='label'>{{getTaskTitle(task.type)}}</label>
<op-drop-down name="tasker{{i}}" [editable]="true" [(ngModel)]="task.tasker"
[source]="taskrOptions">
</op-drop-down>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label for="content{{i}}" class="label">內容</label>
<input type="text" name="content{{i}}" nbInput fullWidth id="content{{i}}"
[(ngModel)]="task.content">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Close
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing">
Save
</button>
</nb-card-footer>
</nb-card>
</form>

View File

@ -1,79 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { HappinessTask, HappinessTaskType, HappinessWeek } from '../../../entity/HappinessGroup';
import { HappinessTaskService } from '../../../services/crudServices/happiness-group.service';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { StringUtils } from '../../../utilities/string-utils';
@Component({
selector: 'ngx-week-task-editor',
templateUrl: './week-task-editor.component.html',
styleUrls: ['./week-task-editor.component.scss']
})
export class WeekTaskEditorComponent implements OnInit {
processing: boolean = false;
allData: HappinessWeek[];
data: HappinessWeek;
taskrOptions: DropDownOption[] = [];
constructor(
private happinessTaskService: HappinessTaskService,
private dlgRef: NbDialogRef<WeekTaskEditorComponent>
) {
}
ngOnInit(): void {
if (this.data.tasks.length == 0) {
this.data.tasks = [];
for (let i = HappinessTaskType.IceBreak; i <= HappinessTaskType.Dessert; i++) {
this.data.tasks.push(
{
weekId: this.data.id,
id: 'new',
type: i as HappinessTaskType,
tasker: '',
content: ''
} as HappinessTask
)
}
}
let names = [] as string[];
this.allData.forEach(week => {
if (week.tasks) {
names = names.concat(week.tasks.map(t => t.tasker.trim()));
}
});
this.taskrOptions = names.filter(function (item, pos) {
return !StringUtils.isNullOrWhitespace(item) && names.indexOf(item) == pos;
}).map(name => new DropDownOption(name, name));
}
close() {
this.dlgRef.close();
}
update() {
if (this.processing == false) {
this.processing = true;
this.happinessTaskService.createOrUpdateAll(this.data.tasks).pipe(first()).subscribe(result => {
this.processing = false;
this.dlgRef.close(true);
});
}
}
getTaskTitle(t: HappinessTaskType) {
switch (t) {
case HappinessTaskType.IceBreak: return '帶遊戲';
case HappinessTaskType.Worship: return '唱歌';
case HappinessTaskType.Testimony: return '見證';
case HappinessTaskType.Message: return '信息';
case HappinessTaskType.Gift: return '準備禮物';
case HappinessTaskType.Dessert: return '準備點心';
default: break;
}
}
}

View File

@ -33,7 +33,7 @@ export class LineMessagingAccountEditorComponent implements OnInit {
this.processing = true;
let func = this.isAdding ? this.lineMessagingAccountService.createOrUpdate(this.data) : this.lineMessagingAccountService.update(this.data);
let func = this.lineMessagingAccountService.createOrUpdate(this.data);//this.isAdding ? this.lineMessagingAccountService.createOrUpdate(this.data) : this.lineMessagingAccountService.update(this.data);
func.pipe(first()).subscribe(result => {
this.processing = false;
if (result) {

View File

@ -3,11 +3,11 @@ import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { StateService } from '../../@core/utils';
import { Log, LogLevel } from '../../entity/Log';
import { ScreenBase } from '../../ScreenBase';
import { LogService } from '../../services/crudServices/log.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancyTag } from '../../ui/fancy-table/fancy-row-column.model';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';

View File

@ -3,17 +3,18 @@ import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { StateService } from '../../@core/utils';
import { FamilyMember } from '../../entity/Member';
import { DomainType, PastoralDomain } from '../../entity/PastoralDomain';
import { BestListDlgComponent } from '../../happiness/best-list-dlg/best-list-dlg.component';
import { HappinessWeekListDlgComponent } from '../../happiness/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { DomainMemberShipService } from '../../services/crudServices/domain-member-ship.service';
import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';
import { ObjectUtils } from '../../utilities/object-utils';
import { BestListDlgComponent } from '../happiness-groups/best-list-dlg/best-list-dlg.component';
import { HappinessWeekListDlgComponent } from '../happiness-groups/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { PastoralDomainEditorComponent } from './pastoral-domain-editor/pastoral-domain-editor.component';
@Component({
@ -35,7 +36,8 @@ export class PastoralDomainsComponent implements OnInit {
private dlgService: NbDialogService,
protected stateService: StateService,
protected route: ActivatedRoute,
private memberService: FamilyMemberService
private memberService: FamilyMemberService,
private memberShipService: DomainMemberShipService
) {
}
@ -87,6 +89,15 @@ export class PastoralDomainsComponent implements OnInit {
callback: (datum, element) => {
this.openHappinessWeekListDlg(datum);
},
},
{
enabled: true,
id: 'members',
title: 'Members',
icon: 'people-outline',
callback: (datum, element) => {
this.openMemberAssignDlg(datum);
},
}
],
onContextMenuOpening: (datum, menuItems) => {
@ -94,9 +105,18 @@ export class PastoralDomainsComponent implements OnInit {
menuItems.find(i => i.id == 'edit').visible = !!datum;
menuItems.find(i => i.id == 'happinessBest').visible = datum.type == DomainType.HappinessGroup;
menuItems.find(i => i.id == 'happinessWeek').visible = datum.type == DomainType.HappinessGroup;
//menuItems.find(i => i.id == 'members').visible = datum.type == DomainType.HappinessGroup;
return datum;
},
columns: [
{
name: 'type',
title: 'Type',
type: 'number',
group: true,
visible: false,
groupValuePrepareFunction: (v) => this.getDomainTypeName(v.type)
},
{
name: 'name',
title: 'Name',
@ -108,7 +128,7 @@ export class PastoralDomainsComponent implements OnInit {
name: 'description',
title: 'Description',
type: 'text',
widthPx: 200,
widthPct: 70,
//valuePrepareFunction: (item) => ,
},
{
@ -136,7 +156,6 @@ export class PastoralDomainsComponent implements OnInit {
// },
],
};
//#endregion
@ -222,5 +241,33 @@ export class PastoralDomainsComponent implements OnInit {
});
}
private getDomainTypeName(type: DomainType): string {
switch (type) {
case DomainType.CellGroup: return "細胞小組";
case DomainType.HappinessGroup: return "幸福小組";
case DomainType.CellGroupCoworker: return "細胞同工";
case DomainType.ChurchCoworker: return "教會事工";
case DomainType.Administrator: return "管理員";
default: break;
}
}
private openMemberAssignDlg(datum: PastoralDomain) {
// this.msgBoxService.showListBox<FamilyMember>(
// `Members in ${datum.name}`,
// this.members,
// 'id',
// datum.familyMembers.map(m => m.id),
// (e) => `${e.firstName} ${e.lastName} (${e.email})`
// ).pipe(first()).subscribe(result => {
// if (result) {
// datum.familyMembers = result;
// this.memberShipService.updateMembersInGroup(datum).pipe(first()).subscribe(result => {
// });
// }
// });
}
}

View File

@ -77,7 +77,7 @@ export const routes: Routes = [
}
},
{
path: 'CellGroup', loadChildren: () => import('./cell-group/cell-group.module').then(m => m.CellGroupModule),
path: 'myapp', loadChildren: () => import('./cell-group/cell-group.module').then(m => m.CellGroupModule),
canActivateChild: [AuthGuardService],
data: {
roles: Role.All

View File

@ -0,0 +1,93 @@
import { ActivatedRoute } from "@angular/router";
import { basename } from "path";
import { Observable, Subject } from "rxjs";
import { ScreenBase } from "../ScreenBase";
import { ICrudService } from "../services/crudServices/crud.service";
import { PastoralDomainService } from "../services/crudServices/pastoral-domain.service";
import { StateService } from "../services/state.service";
import { first } from 'rxjs/operators';
import { PastoralDomain } from "../entity/PastoralDomain";
import { Injectable } from "@angular/core";
@Injectable()
export abstract class MyAppBase {
/**
*
*/
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
// super(pastoralDomainService, stateService, route);
// this.isSnapshotView = true;
}
isLoading: boolean = true;
redirectIfNoDomains: boolean = true;
pageGroupId: string;
private domainInitialized = new Subject();;
ngOnInit(): void {
this.isLoading = true;
// this.route.paramMap.subscribe((params: ParamMap) => {
// this.id = +params.get('id')
// });
this.route.paramMap.pipe(first()).subscribe(params => {
if (params.get('groupId')) {
this.pageGroupId = params.get('groupId');
}
this.pastoralDomainService.initialized.takeUntil(this.domainInitialized).subscribe(result => {
if (result) {
this.domainInitialized.next();
if (this.redirectIfNoDomains) {
if (this.pageGroupId && !this.domains.find(d => d.id == this.pageGroupId)) {
this.pastoralDomainService.joinDomain(this.pageGroupId).pipe(first()).subscribe(result => {
this.pageOnInit();
});
return;
} else if (this.domains.length == 0) {
//TODO:SetUser
return;
}
}
this.pageOnInit();
}
});
});
}
public get domains(): PastoralDomain[] {
return this.pastoralDomainService.domains;
}
abstract pageOnInit(): void;
get saveMethod(): Observable<any> {
throw new Error("Method not implemented.");
}
public get happinessGroup(): PastoralDomain {
return this.pastoralDomainService.myHappinessGroup;
}
public get cellGroup(): PastoralDomain {
return this.pastoralDomainService.myCellGroup;
}
public get currentDomain(): PastoralDomain {
return this.domains.find(d => d.id == this.pageGroupId);
}
public get currentDomainName(): string {
if (this.domains && this.pageGroupId) {
//let group = this.domains.find(d => d.id == this.pageGroupId);
return this.domains.find(d => d.id == this.pageGroupId)?.name;
}
return "";
}
}

View File

@ -2,25 +2,58 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NbMenuItem } from '@nebular/theme';
import { Role } from '../entity/Auth';
import { DomainType } from '../entity/PastoralDomain';
import { CellGroupComponent } from './cell-group.component';
import { DinnerComponent } from './dinner/dinner.component';
import { FinanceComponent } from './finance/finance.component';
import { GoogleLoginComponent } from './google-login/google-login.component';
import { BestListComponent, HappinessWeekListComponent } from './happiness-group/happiness-group.component';
import { PrayerComponent } from './prayer/prayer.component';
import { UserProfileComponent } from './user-profile/user-profile.component';
export class CellGroupRoutingConfig {
public static MENU_ITEMS: NbMenuItem[] = [
{
title: '小組禱告',
title: '細胞小組',
icon: 'people-outline',
link: '/CellGroup/prayer',
home: true,
children: [
{
title: '小組禱告',
//icon: 'people-outline',
link: '/myapp/prayer'
},
{
title: '小組晚餐',
//icon: 'people-outline',
link: '/myapp/dinner',
},
],
data: DomainType.CellGroup
},
{
title: '小組晚餐',
icon: 'people-outline',
link: '/CellGroup/dinner',
},
title: '幸福小組',
icon: 'smiling-face-outline',
children: [
{
title: 'Bests',
//icon: 'people-outline',
link: '/myapp/bests'
},
{
title: '幸福周',
//icon: 'people-outline',
link: '/myapp/happinessWeeks',
},
{
title: '財務表',
//icon: 'people-outline',
link: '/myapp/finance',
},
],
data: DomainType.HappinessGroup
},
]
}
const routes: Routes = [
@ -29,7 +62,16 @@ const routes: Routes = [
children:
[
{ path: 'dinner', component: DinnerComponent, },
{ path: 'dinner/:groupId', component: DinnerComponent, },
{ path: 'prayer', component: PrayerComponent, },
{ path: 'prayer/:groupId', component: PrayerComponent, },
{ path: 'profile', component: UserProfileComponent, },
{ path: 'bests', component: BestListComponent, },
{ path: 'bests/:groupId', component: BestListComponent, },
{ path: 'happinessWeeks', component: HappinessWeekListComponent, },
{ path: 'happinessWeeks/:groupId', component: HappinessWeekListComponent, },
{ path: 'finance/:groupId', component: FinanceComponent, },
{
path: 'management', component: GoogleLoginComponent,
data: {

View File

@ -1,4 +1,4 @@
<ngx-one-column-layout>
<nb-menu [items]="MENU_ITEMS"></nb-menu>
<nb-menu [items]="MENU_ITEMS" tag="NavMenu"></nb-menu>
<router-outlet></router-outlet>
</ngx-one-column-layout>

View File

@ -1,5 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { NbMenuItem } from '@nebular/theme';
import { PastoralDomainService } from '../services/crudServices/pastoral-domain.service';
import { ObjectUtils } from '../utilities/object-utils';
import { CellGroupRoutingConfig } from './cell-group-routing.module';
import { first } from 'rxjs/operators';
@Component({
selector: 'ngx-cell-group',
@ -8,10 +12,45 @@ import { CellGroupRoutingConfig } from './cell-group-routing.module';
})
export class CellGroupComponent implements OnInit {
MENU_ITEMS = CellGroupRoutingConfig.MENU_ITEMS;
constructor() { }
ngOnInit(): void {
MENU_ITEMS = ObjectUtils.CloneValue(CellGroupRoutingConfig.MENU_ITEMS);
constructor(
private pastoralDomainService: PastoralDomainService
) {
let subscription = pastoralDomainService.initialized.subscribe(result => {
if (result) {
this.initializeSideMenu();
subscription.unsubscribe();
}
});
}
ngOnInit(): void {
}
private initializeSideMenu() {
for (let i = 0; i < this.MENU_ITEMS.length; i++) {
const item = this.MENU_ITEMS[i];
this.checkDisplayItem(item);
}
}
private checkDisplayItem(item: NbMenuItem) {
if (item.data != undefined) {
var group = this.pastoralDomainService.domains.find(d => d.type == item.data);
if (!group) {
item.hidden = true;
return;
}
item.title = `${group.name}`;
item.children.forEach(element => {
element.link += `/${group.id}`;
});
} else if (item.children) {
item.children.forEach(element => {
this.checkDisplayItem(element);
});
}
}
}

View File

@ -5,11 +5,19 @@ import { CellGroupRoutingModule } from './cell-group-routing.module';
import { CellGroupComponent } from './cell-group.component';
import { DinnerComponent } from './dinner/dinner.component';
import { FormsModule } from '@angular/forms';
import { NbButtonModule, NbCardModule, NbIconModule, NbInputModule, NbMenuModule, NbSpinnerModule, NbToastrModule, NbToggleModule } from '@nebular/theme';
import { NbActionsModule, NbButtonModule, NbCardModule, NbCheckboxModule, NbIconModule, NbInputModule, NbMenuModule, NbSpinnerModule, NbToastrModule, NbToggleModule } from '@nebular/theme';
import { ThemeModule } from '../@theme/theme.module';
import { GoogleLoginComponent } from './google-login/google-login.component';
import { PrayerComponent } from './prayer/prayer.component';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { DateInputModule } from '../ui/date-input/date-input.module';
import { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { HappinessModule } from '../happiness/happiness.module';
import { BestListComponent, HappinessWeekListComponent } from './happiness-group/happiness-group.component';
import { JoinDomainComponent } from './join-domain/join-domain.component';
import { FinanceComponent } from './finance/finance.component';
import { AddContributionDlgComponent } from './finance/add-contribution-dlg/add-contribution-dlg.component';
import { FancyTableModule } from '../ui/fancy-table/fancy-table.module';
@NgModule({
@ -18,7 +26,12 @@ import { UserProfileComponent } from './user-profile/user-profile.component';
DinnerComponent,
GoogleLoginComponent,
PrayerComponent,
UserProfileComponent
UserProfileComponent,
BestListComponent,
HappinessWeekListComponent,
JoinDomainComponent,
FinanceComponent,
AddContributionDlgComponent
],
imports: [
ThemeModule,
@ -32,8 +45,13 @@ import { UserProfileComponent } from './user-profile/user-profile.component';
NbToastrModule,
NbToggleModule,
NbIconModule,
NbMenuModule
NbMenuModule,
NbActionsModule,
DateInputModule,
NbCheckboxModule,
DropDownListModule,
HappinessModule,
FancyTableModule
]
})
export class CellGroupModule { }

View File

@ -11,14 +11,17 @@ import { AuthService } from '../../services/auth.service';
import { DateUtils } from '../../utilities/date-utils';
import { NumberUtils } from '../../utilities/number-utils';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { ActivatedRoute } from '@angular/router';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MyAppBase } from '../MyAppBase';
import { StateService } from '../../services/state.service';
@Component({
selector: 'ngx-dinner',
templateUrl: './dinner.component.html',
styleUrls: ['./dinner.component.scss']
})
export class DinnerComponent implements OnInit {
export class DinnerComponent extends MyAppBase {
constructor(
private cellGroupRoutineEventsService: CellGroupRoutineEventsService,
private messageService: MsgBoxService,
@ -26,10 +29,12 @@ export class DinnerComponent implements OnInit {
private sessionService: SessionService,
private lineService: LineService,
private cdRef: ChangeDetectorRef,
private headerService: HeaderService,
private authService: AuthService
private authService: AuthService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
cellGroupEvent: CellGroupRoutineEvents;
@ -40,10 +45,12 @@ export class DinnerComponent implements OnInit {
isLoading: boolean = true;
processing: boolean = false;
ngOnInit(): void {
this.headerService.setHeader("晚宴系統")
pageOnInit(): void {
this.stateService.SetPageTitle("晚宴系統");
this.getAllData();
}
getAllData() {
this.isLoading = true;
@ -70,7 +77,7 @@ export class DinnerComponent implements OnInit {
this.processing = false;
this.toastrService.success('菜單更新完成!');
this.lineService.pushCommandMessage('Cac4ac5a8d7fc52daa444d71dc7c360a9', 'dinner');
this.lineService.pushCommandMessage(this.cellGroupEvent.pastoralDomainId, 'dinner');
});
}

View File

@ -0,0 +1,36 @@
<nb-card status="success">
<nb-card-header>
新增奉獻
</nb-card-header>
<nb-card-body>
<div class="row form-group">
<div class="col-6 col-md-4">
<div class='form-group'>
<label for='contributor' class='label'>奉獻人</label>
<input type="text" name="contributor" nbInput fullWidth id="contributor"
[(ngModel)]="contribution.contributor">
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label for="costAmount" class="label">金額</label>
<input type="number" name="costAmount" nbInput fullWidth id="costAmount"
[(ngModel)]="contribution.amount">
</div>
</div>
<div class="col-12 col-md-5">
<div class="form-group">
<label for="costContent" class="label">備註</label>
<input type="text" name="costContent" nbInput fullWidth id="costContent"
[(ngModel)]="contribution.comment">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()">Close</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small" (click)="submit()">Submit</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddContributionDlgComponent } from './add-contribution-dlg.component';
describe('AddContributionDlgComponent', () => {
let component: AddContributionDlgComponent;
let fixture: ComponentFixture<AddContributionDlgComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AddContributionDlgComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddContributionDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { Contribution } from '../../../entity/contribution.model';
import { DropDownOption } from '../../../entity/dropDownOption';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'ngx-add-contribution-dlg',
templateUrl: './add-contribution-dlg.component.html',
styleUrls: ['./add-contribution-dlg.component.scss']
})
export class AddContributionDlgComponent implements OnInit {
taskrOptions: DropDownOption[] = [];
contribution: Contribution;
constructor(
private authService: AuthService,
private dlgRef: NbDialogRef<AddContributionDlgComponent>
) { }
ngOnInit(): void {
this.contribution = { time: new Date() } as Contribution;
}
close() {
this.dlgRef.close();
}
submit() {
if (this.contribution.contributor && this.contribution.amount > 0) {
this.dlgRef.close(this.contribution);
}
}
}

View File

@ -0,0 +1,13 @@
<nb-card [nbSpinner]="isLoading" nbSpinnerStatus="info" nbSpinnerSize="giant">
<nb-card-header>
{{currentDomainName}} 財務表
<nb-actions size="small" fullWidth class="float-right">
<nb-action icon="plus-outline" title="Add New" (click)="addContribution()"></nb-action>
</nb-actions>
</nb-card-header>
<nb-card-body>
<ng-container *ngIf="!isLoading">
<fancy-table [data]="contributions" [settings]="contributionSettings" [footerData]="summary"></fancy-table>
</ng-container>
</nb-card-body>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FinanceComponent } from './finance.component';
describe('FinanceComponent', () => {
let component: FinanceComponent;
let fixture: ComponentFixture<FinanceComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FinanceComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FinanceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,208 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbDialogService, NbToastrService } from '@nebular/theme';
import { Contribution } from '../../entity/contribution.model';
import { AuthService } from '../../services/auth.service';
import { ContributionService } from '../../services/crudServices/contribution.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { LineService } from '../../services/line.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { SessionService } from '../../services/session.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
import { AddContributionDlgComponent } from './add-contribution-dlg/add-contribution-dlg.component';
import { first } from 'rxjs/operators';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { ObjectUtils } from '../../utilities/object-utils';
import { NumberUtils } from '../../utilities/number-utils';
import { StringUtils } from '../../utilities/string-utils';
@Component({
selector: 'ngx-finance',
templateUrl: './finance.component.html',
styleUrls: ['./finance.component.scss']
})
export class FinanceComponent extends MyAppBase {
contributions: Contribution[];
summary: Contribution[];
//#region Table Setting
contributionSettings = <FancySettings<Contribution>>{
contextMenuItems: [
// {
// id: 'add',
// enabled: true,
// title: 'Add New',
// icon: 'plus-circle-outline',
// callback: (datum, element) => {
// this.openEditingDialog(true);
// },
// },
// {
// id: 'edit',
// enabled: true,
// title: 'Edit',
// icon: 'edit',
// callback: (datum, element) => {
// this.openEditingDialog(false);
// },
// },
{
enabled: true,
id: 'delete',
title: 'Delete',
icon: 'trash-2-outline',
callback: (datum, element) => {
this.contributionService.delete(datum.id).pipe(first()).subscribe(result => {
});
},
}],
onContextMenuOpening: (datum, menuItems) => {
menuItems.find(i => i.id == 'delete').visible = !!datum;
//menuItems.find(i => i.id == 'edit').visible = !!datum;
return datum;
},
columns: [
{
name: 'time',
title: 'Time',
type: 'date',
widthPx: 120,
//valuePrepareFunction: (item) => ,
},
{
name: 'contributor',
title: 'Contributor',
type: 'text',
widthPx: 150,
//valuePrepareFunction: (item) => ,
},
{
name: 'amount',
title: 'Amount',
type: 'text',
widthPx: 100,
class: 'text-right',
footerClass: 'text-right',
valuePrepareFunction: getAmountDisplay,
},
{
name: 'comment',
title: 'Comment',
type: 'text',
widthPx: 200,
//valuePrepareFunction: (item) => ,
},
],
showFooter: true
};
//#endregion
constructor(
private contributionService: ContributionService,
private messageService: MsgBoxService,
private toastrService: NbToastrService,
private sessionService: SessionService,
private lineService: LineService,
private cdRef: ChangeDetectorRef,
private authService: AuthService,
private dlgService: NbDialogService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.initializeData();
}
addContribution() {
this.dlgService.open(AddContributionDlgComponent, {
context: {
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
result.groupId = this.currentDomain.id;
this.isLoading = true;
this.contributionService.createOrUpdate(result).pipe(first()).subscribe(result => {
this.refreshData();
});
}
});
}
private refreshData() {
this.isLoading = true;
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
this.initializeData();
});
}
private initializeData() {
this.contributions = ObjectUtils.CloneValue(this.currentDomain.contributions);
this.summary = [];
let totalComment = '';
let totalAmount = this.contributions.map(d => d.amount).reduce((a, b) => a + b, 0);
//Append Cost
if (this.currentDomain.happinessWeeks) {
for (let i = 0; i < this.currentDomain.happinessWeeks.length; i++) {
const week = this.currentDomain.happinessWeeks[i];
let weekCost = 0;
for (let j = 0; j < week.costs.length; j++) {
const weekCostItem = week.costs[j];
this.contributions.push(
{
time: week.date,
contributor: `W${i + 1} - ${weekCostItem.tasker}`,
amount: -1 * weekCostItem.amount,
comment: weekCostItem.content
} as Contribution);
weekCost += weekCostItem.amount;
}
if (weekCost > 0) {
this.contributions.push(
{
time: week.date,
contributor: `${week.topic} - Sub Total`,
amount: 0,
comment: '$ ' + NumberUtils.FormatCurrency(weekCost)
} as Contribution);
}
}
let remainWeeks = this.currentDomain.happinessWeeks.filter(w => w.date >= new Date()).length;
totalAmount = this.contributions.map(d => d.amount).reduce((a, b) => a + b, 0);
totalComment = StringUtils.getHtmlBadge(`剩餘每周(${remainWeeks}) 預算: $${NumberUtils.FormatCurrency(totalAmount / remainWeeks)}`, 'info');
}
this.summary.push(
{
time: null,
contributor: `Summary`,
amount: totalAmount,
comment: totalComment
} as Contribution);
this.isLoading = false;
}
}
function getAmountDisplay(item: Contribution): string {
if (item.amount != 0) {
return StringUtils.getHtmlBadge(NumberUtils.FormatCurrency(item.amount), item.amount > 0 ? 'success' : 'warning');
}
return "---------------";
}

View File

@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
@Component({
selector: 'ngx-happiness-group',
template: `
<best-list [group]="happinessGroup"></best-list>
`,
styleUrls: ['./happiness-group.component.scss']
})
export class BestListComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.stateService.SetPageTitle("Best");
}
}
@Component({
selector: 'ngx-happiness-week-group',
template: `<happiness-week-list [group]="happinessGroup"></happiness-week-list>
`,
styleUrls: ['./happiness-group.component.scss']
})
export class HappinessWeekListComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.stateService.SetPageTitle("Happiness Weeks");
}
}

View File

@ -0,0 +1,17 @@
<nb-card status="success">
<nb-card-header>
加入小組
</nb-card-header>
<nb-card-body>
<div class='col-12 col-md-6'>
<div class='form-group'>
<label for='domain' class='label'>小組</label>
<op-drop-down name='domain' id='domain' [(ngModel)]='domainId' [source]='domainOptions'></op-drop-down>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right mr-2" fullWidth nbButton hero status="primary" size="large"
(click)="joinDomain()">加入小組</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JoinDomainComponent } from './join-domain.component';
describe('JoinDomainComponent', () => {
let component: JoinDomainComponent;
let fixture: ComponentFixture<JoinDomainComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ JoinDomainComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(JoinDomainComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DomainType, PastoralDomain } from '../../entity/PastoralDomain';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../entity/dropDownOption';
@Component({
selector: 'ngx-join-domain',
templateUrl: './join-domain.component.html',
styleUrls: ['./join-domain.component.scss']
})
export class JoinDomainComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
this.redirectIfNoDomains = false;
}
domainId: string;
allDomains: PastoralDomain[];
domainOptions: DropDownOption[] = [];
pageOnInit(): void {
this.isLoading = true;
this.pastoralDomainService.getAll().pipe(first()).subscribe(result => {
this.allDomains = result.filter(d => d.type == DomainType.CellGroup || d.type == DomainType.HappinessGroup);
this.domainOptions = this.allDomains.map(d => new DropDownOption(d.id, d.name));
this.isLoading = false;
});
}
joinDomain() {
}
}

View File

@ -1,37 +1,38 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { BindingHelper } from '../../entity/BindingHelper';
import { CellGroupRoutineEvents, CellGroupRoutineEventPrayer, CellGroupRoutineEventAttendee } from '../../entity/CellGroupRoutineEvents';
import { AuthService } from '../../services/auth.service';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { HeaderService } from '../../services/header.service';
import { LineService } from '../../services/line.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { SessionService } from '../../services/session.service';
import { StateService } from '../../services/state.service';
import { DateUtils } from '../../utilities/date-utils';
import { NumberUtils } from '../../utilities/number-utils';
import { DinnerInfo } from '../dinner/dinner.component';
import { MyAppBase } from '../MyAppBase';
@Component({
selector: 'ngx-prayer',
templateUrl: './prayer.component.html',
styleUrls: ['./prayer.component.scss']
})
export class PrayerComponent implements OnInit {
export class PrayerComponent extends MyAppBase {
constructor(
private cellGroupRoutineEventsService: CellGroupRoutineEventsService,
private messageService: MsgBoxService,
private toastrService: NbToastrService,
private sessionService: SessionService,
private lineService: LineService,
private cdRef: ChangeDetectorRef,
private headerService: HeaderService,
private authService: AuthService
private authService: AuthService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
cellGroupEvent: CellGroupRoutineEvents;
@ -42,8 +43,8 @@ export class PrayerComponent implements OnInit {
isLoading: boolean = true;
processing: boolean = false;
ngOnInit(): void {
this.headerService.setHeader("禱告中心")
pageOnInit(): void {
this.stateService.SetPageTitle("禱告中心");
this.getAllData();
}
getAllData() {

View File

@ -5,20 +5,21 @@
</nb-card-header>
<nb-card-body>
<div class="row" *ngIf="data">
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="form-group">
<label for="firstName" class="label">First Name</label>
<input type="text" name="firstName" nbInput fullWidth id="firstName"
[(ngModel)]="data.firstName">
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="form-group">
<label for="lastName" class="label">Last Name</label>
<input type="text" name="lastName" nbInput fullWidth id="lastName" [(ngModel)]="data.lastName">
</div>
</div>
<div class="col-md-2">
<div class="col-4 col-md-2">
<div class="form-group">
<label for="gender" class="label">Gender</label>
<op-drop-down id="gender" name="gender" [(ngModel)]='data.gender' [source]="GenderOptions">
@ -27,25 +28,21 @@
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<nb-checkbox id="married" name="married" [(ngModel)]="data.married">Married</nb-checkbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<nb-checkbox id="baptized" name="baptized" [(ngModel)]="data.baptized">Baptized</nb-checkbox>
</div>
</div>
<div class="col-md-6">
<div class="col-8 col-md-4">
<div class='form-group'>
<label for='birthday' class='label'>Birthday</label>
<op-date-input id='birthday' name='birthday' [(ngModel)]='data.birthday'></op-date-input>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group">
<label for="email" class="label">Email</label>
<input type="text" name="email" nbInput fullWidth id="email" [(ngModel)]="data.email" readonly>
</div>
</div>
<div class="col-md-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label for="address" class="label">Address</label>
<input type="text" name="address" nbInput fullWidth id="address" [(ngModel)]="data.address">
@ -58,30 +55,30 @@
[(ngModel)]="data.avatarImage">
</div>
</div> -->
<div class="col-md-4">
<div class='form-group'>
<label for='birthday' class='label'>Birthday</label>
<input type='text' nbInput fullWidth id='birthday' name='birthday' [(ngModel)]='data.birthday'
[nbDatepicker]="dateTimePickerbirthday">
<nb-date-timepicker format="yyyy/MM/dd" #dateTimePickerbirthday></nb-date-timepicker>
<div class="col-6 col-md-2 align-self-md-end">
<div class="form-group">
<nb-checkbox id="married" name="married" [(ngModel)]="data.married">Married</nb-checkbox>
</div>
</div>
<div class="col-md-4">
<div class='form-group'>
<label for='dateOfBaptized' class='label'>Date Of Baptized</label>
<input type='text' nbInput fullWidth id='dateOfBaptized' name='dateOfBaptized'
[(ngModel)]='data.dateOfBaptized' [nbDatepicker]="dateTimePickerdateOfBaptized">
<nb-datepicker format="yyyy/MM/dd" #dateTimePickerdateOfBaptized></nb-datepicker>
<div class="col-6 col-md-2 align-self-md-end">
<div class="form-group">
<nb-checkbox id="baptized" name="baptized" [(ngModel)]="data.baptized">Baptized</nb-checkbox>
</div>
</div>
<div class="col-md-4">
<div class="col-12 col-md-4">
<div class='form-group'>
<label for='dateOfWalkIn' class='label'>Date Of Walk In</label>
<input type='text' nbInput fullWidth id='dateOfWalkIn' name='dateOfWalkIn'
[(ngModel)]='data.dateOfWalkIn' [nbDatepicker]="dateTimePickerdateOfWalkIn">
<nb-date-timepicker format="yyyy/MM/dd" #dateTimePickerdateOfWalkIn></nb-date-timepicker>
<op-date-input id='dateOfWalkIn' name='dateOfWalkIn' [(ngModel)]='data.dateOfWalkIn'>
</op-date-input>
</div>
</div>
<div class="col-12 col-md-4">
<div class='form-group' *ngIf="data.baptized">
<label for='dateOfBaptized' class='label'>Date Of Baptized</label>
<op-date-input id='dateOfBaptized' name='dateOfBaptized' [(ngModel)]='data.dateOfBaptized'>
</op-date-input>
</div>
</div>
</div>
@ -89,10 +86,10 @@
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
<!-- <button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Cancel
</button>
</button> -->
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing">
Save

View File

@ -4,22 +4,29 @@ import { AuthService } from '../../services/auth.service';
import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { SessionService } from '../../services/session.service';
import { first } from "rxjs/operators";
import { DropDownOptions } from '../../entity/dropDownOption';
import { NbToastrService } from '@nebular/theme';
@Component({
selector: 'ngx-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
GenderOptions = DropDownOptions.GenderOptions;
constructor(
private memberService: FamilyMemberService,
private authService: AuthService
private authService: AuthService,
private toastrService: NbToastrService,
) { }
data: FamilyMember;
processing: boolean;
ngOnInit(): void {
this.memberService.getById(this.authService.userAccess.memberId).pipe(first()).subscribe(result => {
this.data = result;
if (this.data.dateOfBaptized == undefined) this.data.dateOfBaptized = null;
if (this.data.dateOfWalkIn == undefined) this.data.dateOfWalkIn = null;
if (this.data.birthday == undefined) this.data.birthday = null;
});
}
@ -29,6 +36,7 @@ export class UserProfileComponent implements OnInit {
update() {
this.memberService.createOrUpdate(this.data).pipe(first()).subscribe(result => {
this.toastrService.success('更新完成!', '個人資料');
});
}
}

View File

@ -4,6 +4,7 @@ export interface CellGroupRoutineEvents {
address: string;
attendees: CellGroupRoutineEventAttendee[];
prayers: CellGroupRoutineEventPrayer[];
pastoralDomainId: string;
}

View File

@ -1,3 +1,4 @@
import { HappinessCost } from "./happiness-cost.model";
import { PastoralDomain } from "./PastoralDomain";
export interface HappinessWeek {
@ -11,6 +12,7 @@ export interface HappinessWeek {
seq: number;
updateRestWeekDate: boolean;
tasks: HappinessTask[];
costs: HappinessCost[];
topic: string
}

View File

@ -1,3 +1,4 @@
import { Contribution } from "./contribution.model";
import { HappinessBEST, HappinessWeek } from "./HappinessGroup";
import { FamilyMember } from "./Member";
@ -6,7 +7,7 @@ export enum DomainType {
HappinessGroup,
CellGroupCoworker,
ChurchCoworker,
Person = 99
Administrator = 99
}
export interface PastoralDomain {
@ -27,6 +28,7 @@ export interface PastoralDomain {
bests: HappinessBEST[];
happinessWeeks: HappinessWeek[];
contributions: Contribution[];
serviceAddress: AddressInfo;
type: DomainType;

View File

@ -0,0 +1,7 @@
import { Contribution } from './contribution.model';
describe('Contribution', () => {
it('should create an instance', () => {
expect(new Contribution()).toBeTruthy();
});
});

View File

@ -0,0 +1,8 @@
export interface Contribution {
groupId: string;
id: string;
contributor: string;
amount: number;
comment: string;
time: Date;
}

View File

@ -0,0 +1,7 @@
import { HappinessCost } from './happiness-cost.model';
describe('HappinessCost', () => {
it('should create an instance', () => {
expect(new HappinessCost()).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
export class HappinessCost {
weekId: string;
id: string;
tasker: string;
content: string;
amount: number;
}

View File

@ -8,12 +8,12 @@ import * as signalR from "@microsoft/signalr"
import { StringUtils } from '../../utilities/string-utils';
import { ActivatedRoute } from '@angular/router';
import { MsgBoxService } from '../../services/msg-box.service';
import { ADButtons, ADIcon } from '../../ui/alert-dlg/alert-dlg.component';
import { UuidUtils } from '../../utilities/uuid-utils';
import { ObjectUtils } from '../../utilities/object-utils';
import { NbToastrService } from '@nebular/theme';
import { AvalonBase } from './avalonBase';
import { HeaderService } from '../../services/header.service';
import { ADIcon, ADButtons } from '../../ui/alert-dlg/alert-dlg.model';
const minimumPlayers = 5;
const maximumPlayers = 10;
@ -181,7 +181,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
public showMyRole() {
let roleInfo = this.getRoleInfoByRole(this.me.role);
this.msgBoxService.show(roleInfo.name, roleInfo.description, ADIcon.INFO)
this.msgBoxService.show(roleInfo.name, { text: roleInfo.description, icon: ADIcon.INFO });
}
public showMySecret() {
//let roleInfo = this.getRoleInfoByRole(this.me.role);
@ -208,7 +208,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
break;
}
this.msgBoxService.show('噓...', description, ADIcon.INFO);
this.msgBoxService.show('噓...', { text: description, icon: ADIcon.INFO });
}
private initializeQuestInfo() {
@ -251,7 +251,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
previousStage() {
this.msgBoxService.show('上一棟', '你確定?', ADIcon.QUESTION, ADButtons.YesNo).pipe(first()).subscribe(result => {
this.msgBoxService.show('上一棟', { text: '你確定?', icon: ADIcon.QUESTION, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) {
this.avalonService.data.stage -= 1;
this.clearVoteStatus();
@ -264,14 +264,14 @@ export class AvalonComponent extends AvalonBase implements OnInit {
switch (this.avalonService.data.stage) {
case AvalonStage.JoinGame:
if (this.players.length < 5) {
this.msgBoxService.show("人數不足!", "遊戲最少需要五人");
this.msgBoxService.show("人數不足!", { text: "遊戲最少需要五人" });
return;
}
this.gameSize = Math.max(0, this.players.length - 6);
if (this.avalonService.data.roles.filter(r => r.enabled && false == r.isGood).length != players_evil[this.gameSize]) {
this.msgBoxService.show("反派人數錯誤!", "反派人數需要 " + players_evil[this.gameSize].toString() + " 人");
this.msgBoxService.show("反派人數錯誤!", { text: "反派人數需要 " + players_evil[this.gameSize].toString() + " 人" });
return;
}

View File

@ -4,7 +4,7 @@ import { first } from 'rxjs/operators';
import { GameInfo, Player, Role, RoleInfo } from '../../../entity/Avalon';
import { AvalonService } from '../../../services/avalon.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { ADButtons, ADIcon } from '../../../ui/alert-dlg/alert-dlg.component';
import { ADButtons, ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { AvalonBase } from '../avalonBase';
@Component({
@ -38,7 +38,7 @@ export class ChooseCharacterComponent extends AvalonBase implements OnInit {
// return;
this.ngZone.run(
_ => {
this.msgBoxService.show(roleInfo.name, roleInfo.description + '<br>你確定你是?', ADIcon.QUESTION, ADButtons.YesNo).pipe(first()).subscribe(result => {
this.msgBoxService.show(roleInfo.name, { text: roleInfo.description + '<br>你確定你是?', icon: ADIcon.QUESTION, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) {
if (this.players.find(p => p.role == roleInfo.role) && roleInfo.role != Role.ArthurKnight) {
this.toastrService.danger('腳色重複:' + roleInfo.name, '你確定沒有選錯腳色??')

View File

@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, NgZone, OnInit } from '@angular/core';
import { Player, AvalonStage } from '../../../entity/Avalon';
import { AvalonService } from '../../../services/avalon.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.component';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { AvalonBase } from '../avalonBase';
@Component({
@ -38,7 +38,7 @@ export class PickTeammateComponent extends AvalonBase implements OnInit {
this.avalonService.applyCdChange$.next();
} else {
this.ngZone.run(_ => {
this.msgBoxService.show('任務編組錯誤!', `本次任務出場人數為 ${this.currentQuest.teamSize} 人!`, ADIcon.WARNING);
this.msgBoxService.show('任務編組錯誤!', { text: `本次任務出場人數為 ${this.currentQuest.teamSize} 人!`, icon: ADIcon.WARNING });
});
}
}

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AvalonComponent } from './avalon/avalon.component';
import { GamesComponent } from './games.component';
import { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.component';
const routes: Routes = [
{
@ -12,6 +13,7 @@ const routes: Routes = [
[
{ path: 'avalon', component: AvalonComponent },
{ path: 'avalonHost', component: AvalonComponent },
{ path: 'MD2', component: MassiveDarkness2Component },
]
},

View File

@ -16,6 +16,7 @@ import { TeamVoteComponent } from './avalon/team-vote/team-vote.component';
import { QuestVoteComponent } from './avalon/quest-vote/quest-vote.component';
import { VoteResultComponent } from './avalon/vote-result/vote-result.component';
import { QuestTableComponent } from './avalon/quest-table/quest-table.component';
import { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.component';
@NgModule({
@ -28,7 +29,8 @@ import { QuestTableComponent } from './avalon/quest-table/quest-table.component'
TeamVoteComponent,
QuestVoteComponent,
VoteResultComponent,
QuestTableComponent
QuestTableComponent,
MassiveDarkness2Component
],
imports: [
CommonModule,

View File

@ -0,0 +1,25 @@
<div class="row">
<div class="col-12 col-md-4">
<nb-card>
<nb-card-header>
Treasure Bag
<button nbButton hero status="primary" size="small">Reset</button>
<button nbButton hero status="primary" size="small" (click)="addTreasure(TreasureType.Rare)">Add
Rare</button>
<button nbButton hero status="primary" size="small" (click)="addTreasure(TreasureType.Epic)">Add
Epic</button>
</nb-card-header>
<nb-card-body>
<div>
<ng-template *ngFor="let treasure of treasureBag.drawingItems">
<img src="{{treasure.imageUrl}}"> X {{treasure.unitAmount}}
</ng-template>
</div>
<button nbButton hero status="primary">I feel LUCKY!!!!!</button>
</nb-card-body>
</nb-card>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MassiveDarkness2Component } from './massive-darkness2.component';
describe('MassiveDarkness2Component', () => {
let component: MassiveDarkness2Component;
let fixture: ComponentFixture<MassiveDarkness2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MassiveDarkness2Component ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MassiveDarkness2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,143 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FileService } from '../../services/file.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { ArrayUtils } from '../../utilities/array-utils';
import { ObjectUtils } from '../../utilities/object-utils';
@Component({
selector: 'ngx-massive-darkness2',
templateUrl: './massive-darkness2.component.html',
styleUrls: ['./massive-darkness2.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MassiveDarkness2Component implements OnInit {
TreasureType = TreasureType;
treasureBag: DrawingBag = new DrawingBag();
constructor(
private fileService: FileService,
private cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService
) { }
ngOnInit(): void {
this.resetTreasureBag();
this.detectChanges();
}
detectChanges() {
if (!this.cdRef['destroyed']) {
this.cdRef.detectChanges();
}
}
resetTreasureBag() {
this.treasureBag.ClearAllItems();
this.addTreasure(TreasureType.Common, 15);
}
addTreasure(type: TreasureType, amount: number = 1) {
let item = new DrawingItem(`${TreasureType[type]} Treasure`, `It's a ${TreasureType[type]} Treasure!`, this.fileService.ImageUrl(`TreasureToken/${TreasureType[type]}.png`), amount);
this.treasureBag.AddItem(item);
this.detectChanges();
}
treasureImage(type: TreasureType) {
return this.fileService.ImageUrl(`TreasureToken/${TreasureType[type]}.png`);
}
drawTreasure() {
}
}
export enum TreasureType {
Common,
Rare,
Epic,
Legendary
}
export class DrawingBag {
constructor() {
this.drawingItems = [];
this.removedItems = [];
}
drawingItems: DrawingItem[]
removedItems: DrawingItem[]
public Draw(amount: number): DrawingItem[] {
let drawItems: DrawingItem[] = [];
for (let i = 0; i < amount; i++) {
drawItems.push(this.DrawAndRemove());
}
this.RestoreRemoveItems();
return drawItems;
}
public DrawAndRemove(): DrawingItem {
if (this.drawingItems.length > 0) {
let drawItem = null as DrawingItem;
let drawIndex = Math.random() * this.drawingItems.reduce((sum, current) => sum + current.unitAmount, 0);
let drawCalc = 0;
for (let i = 0; i < this.drawingItems.length; i++) {
const item = this.drawingItems[i];
drawCalc += item.unitAmount;
if (drawCalc >= drawIndex) {
drawItem = ObjectUtils.CloneValue(item);
drawItem.unitAmount = 1;
item.unitAmount -= 1;
break;
}
}
//ObjectUtils.CloneValue
this.RemoveItem(drawItem);
return drawItem;
}
return null;
}
public RestoreRemoveItems() {
for (let i = 0; i < this.removedItems.length; i++) {
const removedItem = this.removedItems[i];
this.AddItem(removedItem);
}
}
public AddItem(item: DrawingItem) {
let existingItem = this.drawingItems.find(i => i.name == item.name);
if (existingItem) {
existingItem.unitAmount += item.unitAmount;
} else {
this.drawingItems.push(item);
}
}
public RemoveItem(item: DrawingItem) {
let existingItem = this.removedItems.find(i => i.name == item.name);
if (existingItem) {
existingItem.unitAmount += item.unitAmount;
} else {
this.removedItems.push(item);
}
}
public ClearAllItems() {
this.drawingItems = [];
this.removedItems = [];
}
}
export class DrawingItem {
constructor(
name: string,
description: string,
imageUrl: string,
unitAmount: number = 1
) {
this.imageUrl = imageUrl
this.name = name
this.description = description
this.unitAmount = unitAmount
}
imageUrl: string
name: string
description: string
unitAmount: number
}

View File

@ -0,0 +1,42 @@
<form #form="ngForm">
<nb-card>
<nb-card-header>
{{data.name}}
</nb-card-header>
<nb-card-body>
<!-- <loading-spinner [isLoading]="isLoading"></loading-spinner> -->
<div class="row" *ngIf="data">
<div class="col-12 col-md-6">
<div class="form-group">
<label for="name" class="label">Name</label>
<input type="text" name="name" nbInput fullWidth id="name" [(ngModel)]="data.name">
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group">
<label for="phone" class="label">Phone</label>
<input type="text" name="phone" nbInput fullWidth id="phone" [(ngModel)]="data.phone">
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="email" class="label">Email</label>
<input type="text" name="email" nbInput fullWidth id="email" [(ngModel)]="data.email">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Close
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing">
Save
</button>
</nb-card-footer>
</nb-card>
</form>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BestEditorDlgComponent } from './best-editor-dlg.component';
describe('BestEditorDlgComponent', () => {
let component: BestEditorDlgComponent;
let fixture: ComponentFixture<BestEditorDlgComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BestEditorDlgComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BestEditorDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { HappinessBEST } from '../../../entity/HappinessGroup';
import { BestService } from '../../../services/crudServices/best.service';
@Component({
selector: 'ngx-best-editor-dlg',
templateUrl: './best-editor-dlg.component.html',
styleUrls: ['./best-editor-dlg.component.scss']
})
export class BestEditorDlgComponent implements OnInit {
isAdding: boolean = false;
processing: boolean = false;
data: HappinessBEST;
constructor(
private bestService: BestService,
private dlgRef: NbDialogRef<BestEditorDlgComponent>
) {
}
ngOnInit(): void {
if (this.isAdding) {
this.data = { id: 'new' } as HappinessBEST;
}
}
close() {
this.dlgRef.close();
}
update() {
if (this.processing == false) {
this.processing = true;
let func = this.bestService.update(this.data);//this.isAdding ? this.bestService.create(this.data) :
func.pipe(first()).subscribe(result => {
this.processing = false;
if (result) {
this.dlgRef.close(true);
}
});
}
}
}

View File

@ -1,10 +1,13 @@
<nb-card>
<nb-card [ngClass]="{'isDlg': isDlg}">
<nb-card-header>
Happiness Group:{{group.name}} Best List
{{group.name}} Best List
<button class="float-right" nbButton hero status="success" size="small" (click)="addBest()"
[nbSpinner]="processing">Create New</button>
<button class="float-right mr-2" nbButton hero status="success" size="small" (click)="copyBestList()"
[nbSpinner]="processing">Copy Whole List</button>
<button class="float-right mr-2" nbButton hero status="success" size="small" (click)="pushListToLine()"
[nbSpinner]="processing">Push List To Line</button>
<!-- <button class="float-right mr-2" nbButton hero status="success" size="small" (click)="copyBestList()"
[nbSpinner]="processing">Copy Whole List</button> -->
</nb-card-header>
<nb-card-body [nbSpinner]="processing">
@ -24,8 +27,8 @@
(click)="openEditDlg(best)">Edit</button>
<button nbButton hero status="primary" class="mr-1" size="small"
(click)="copyBestUrl(best)">Copy Url</button>
<button nbButton hero status="primary" class="" size="small"
(click)="downloadBestQRCode(best)">QR Code</button>
<button nbButton hero status="primary" class="" size="small" (click)="downloadBestQRCode(best)"
*ngIf="isDlg">QR Code</button>
</div>
</div>

View File

@ -0,0 +1,7 @@
nb-card {
max-height: 74vh;
.isDlg{
max-height: 90vh;
width: 700px;
}
}

View File

@ -0,0 +1,108 @@
import { Component, Input, OnInit, Optional } from '@angular/core';
import { NbDialogRef, NbDialogService, NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { HappinessBEST } from '../../entity/HappinessGroup';
import { PastoralDomain } from '../../entity/PastoralDomain';
import { BestService } from '../../services/crudServices/best.service';
import { HappinessGroupService } from '../../services/crudServices/happiness-group.service';
import { LineService, LineGroup } from '../../services/line.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { ADButtons, ADIcon } from '../../ui/alert-dlg/alert-dlg.model';
import { ObjectUtils } from '../../utilities/object-utils';
import { BestEditorDlgComponent } from './best-editor-dlg/best-editor-dlg.component';
@Component({
selector: 'best-list',
templateUrl: './best-list-dlg.component.html',
styleUrls: ['./best-list-dlg.component.scss']
})
export class BestListDlgComponent implements OnInit {
processing: boolean = false;
isAdding: boolean = false;
@Input() group: PastoralDomain;
datum: HappinessBEST;
public get isDlg(): boolean {
return this.dlgRef != null;
}
constructor(
private msgBpxService: MsgBoxService,
private happinessGroupService: HappinessGroupService,
private toastrService: NbToastrService,
private bestService: BestService,
private lineService: LineService,
private dlgService: NbDialogService,
@Optional() private dlgRef: NbDialogRef<BestListDlgComponent>,
) { }
ngOnInit(): void {
this.group.bests = this.group.bests.sort((a, b) => 0 - (a.name < b.name ? 1 : -1));
}
close() {
this.dlgRef.close();
}
addBest() {
this.msgBpxService.showInputbox('Add Best', 'Please input Best\'s Name').pipe(first()).subscribe(result => {
if (result) {
let obj = { groupId: this.group.id, id: '', name: result } as HappinessBEST;
this.bestService.createOrUpdate(obj).pipe(first()).subscribe(result => {
this.group.bests.push(result);
});
}
});
}
copyBestUrl(best: HappinessBEST) {
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = `${environment.invitationUrl}${best.id}`;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
}
copyBestList() {
let list = "".concat(...this.group.bests.map(b => `${b.name}\t\t: ${environment.invitationUrl}${b.id}\r\n`));
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = list;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
//this.lineService.pushLineMessage(LineGroup.Chris, list);
}
pushListToLine() {
this.msgBpxService.show('Push Whole List To Line', { text: 'Are You Sure?', icon: ADIcon.QUESTION, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) {
this.lineService.pushCommandMessage(this.group.id, 'BEST');
this.toastrService.success('Sent', "Sent To Line");
}
});
}
openEditDlg(best: HappinessBEST) {
let isAdding = false;
this.dlgService.open(BestEditorDlgComponent, {
context: {
data: isAdding ? null : ObjectUtils.CloneValue(best),
isAdding: isAdding
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
ObjectUtils.CopyValue(result, best);
}
});
}
downloadBestQRCode(best: HappinessBEST) {
this.happinessGroupService.getBestQrCode(best.name + ".png", best.id);
}
}

View File

@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { HappinessWeek } from '../../../entity/HappinessGroup';
import { HappinessGroupService } from '../../../services/crudServices/happiness-group.service';
import { HappinessWeek } from '../../entity/HappinessGroup';
import { HappinessGroupService } from '../../services/crudServices/happiness-group.service';
@Component({
selector: 'ngx-happiness-week-editor',
templateUrl: './happiness-week-editor.component.html',
@ -13,6 +12,7 @@ export class HappinessWeekEditorComponent implements OnInit {
isAdding: boolean = false;
processing: boolean = false;
data: HappinessWeek;
allWeeks: HappinessWeek[];
constructor(
private happinessGroupService: HappinessGroupService,
private dlgRef: NbDialogRef<HappinessWeekEditorComponent>

View File

@ -1,25 +1,30 @@
<nb-card>
<nb-card [nbSpinner]="processing">
<nb-card-header>
Happiness Group:{{group.name}} Weeks List
{{group.name}} 幸福周
<button nbButton hero status="primary" class="float-right" size="small" (click)="openTaskDlg(currentWeek)"
*ngIf="currentWeek">{{currentWeek.topic}} 分工</button>
</nb-card-header>
<nb-card-body>
<nb-list>
<nb-list-item *ngFor="let week of group.happinessWeeks" class="d-block">
<div class="row">
<div class="row text-center text-md-left">
<div class="col-md-6">
{{weekDisplay(week.seq)}}
<p [outerHTML]="weekDisplay(week.seq)"></p>
<!-- <nb-user [name]="weekDisplay(week.seq)">
</nb-user> -->
</div>
<div class="col-md text-right">
<div class="col-md mt-2 mt-md-0 text-md-right">
<button nbButton hero status="primary" class="mr-1" size="small"
(click)="openEditDlg(week)">Edit</button>
<button nbButton hero status="primary" class="mr-1" size="small"
(click)="openTaskDlg(week)">Tasks</button>
(click)="openTaskDlg(week,false)">Tasks</button>
<button nbButton hero status="primary" class="mr-1" size="small"
(click)="openTaskDlg(week,true)">Costs</button>
</div>
@ -30,6 +35,7 @@
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()">Close</button>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
*ngIf="isDlg">Close</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,9 @@
nb-card {
max-height: 74vh;
width: 615px;
max-width: 100vw;
.isDlg {
max-height: 90vh;
width: 700px;
}
}

View File

@ -0,0 +1,100 @@
import { Component, Input, OnInit, Optional } from '@angular/core';
import { NbDialogRef, NbDialogService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { HappinessWeek } from '../../entity/HappinessGroup';
import { PastoralDomain } from '../../entity/PastoralDomain';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { NumberUtils } from '../../utilities/number-utils';
import { StringUtils } from '../../utilities/string-utils';
import { HappinessWeekEditorComponent } from '../happiness-week-editor/happiness-week-editor.component';
import { WeekTaskEditorComponent } from '../week-task-editor/week-task-editor.component';
@Component({
selector: 'happiness-week-list',
templateUrl: './happiness-week-list-dlg.component.html',
styleUrls: ['./happiness-week-list-dlg.component.scss']
})
export class HappinessWeekListDlgComponent implements OnInit {
processing: boolean = false;
private _group: PastoralDomain;
public get group(): PastoralDomain {
return this._group;
}
@Input() public set group(v: PastoralDomain) {
if (this._group != v) {
this._group = v;
this.currentWeek = this.group.happinessWeeks.find(w => w.date >= new Date());
}
}
currentWeek: HappinessWeek
public get isDlg(): boolean {
return this.dlgRef != null;
}
constructor(
private dlgService: NbDialogService,
private pastoralDomainService: PastoralDomainService,
@Optional() private dlgRef: NbDialogRef<any>
) { }
ngOnInit(): void {
}
close() {
this.dlgRef.close();
}
openEditDlg(week: HappinessWeek) {
this.dlgService.open(HappinessWeekEditorComponent, {
context: {
data: week,
allWeeks: this.group.happinessWeeks
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
if (this.isDlg) {
this.close();
} else {
this.runGetAllData();
}
}
if (result) this.close();
});
}
openTaskDlg(week: HappinessWeek, showingCost: boolean) {
this.dlgService.open(WeekTaskEditorComponent, {
context: {
data: week,
showingCost: showingCost,
allData: this.group.happinessWeeks
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
if (this.isDlg) {
this.close();
} else {
this.runGetAllData();
}
}
});
}
runGetAllData() {
this.processing = true;
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
this.group = this.pastoralDomainService.myHappinessGroup;
this.processing = false;
});
}
weekDisplay(seq: number) {
let week = this.group.happinessWeeks[seq - 1];
let title = `W${seq} ${week.topic}`;
let weekCost = week.costs.map(d => d.amount).reduce((a, b) => a + b, 0);
if (weekCost > 0) {
title += "\t" + StringUtils.getHtmlBadge(`Cost: $${NumberUtils.FormatCurrency(weekCost)}`, 'warning')
}
return title;
}
}

View File

@ -0,0 +1,68 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbUserModule, NbSpinnerModule, NbDialogModule, NbAlertModule } from '@nebular/theme';
import { NgxMaskModule } from 'ngx-mask';
import { ThemeModule } from '../@theme/theme.module';
import { MaskDirectiveModule } from '../directives/mask-directive/mask-directive.module';
import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module';
import { CurrencyInputModule } from '../ui/currency-input/currency-input.module';
import { DateInputModule } from '../ui/date-input/date-input.module';
import { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { FancyTableModule } from '../ui/fancy-table/fancy-table.module';
import { WeekTaskEditorComponent } from './week-task-editor/week-task-editor.component';
import { HappinessWeekEditorComponent } from './happiness-week-editor/happiness-week-editor.component';
import { BestListDlgComponent } from './best-list-dlg/best-list-dlg.component';
import { HappinessWeekListDlgComponent } from './happiness-week-list-dlg/happiness-week-list-dlg.component';
import { BestEditorDlgComponent } from './best-list-dlg/best-editor-dlg/best-editor-dlg.component';
import { LoadingSpinnerModule } from '../ui/loading-spinner/loading-spinner.module';
import { AddNewCostDlgComponent } from './week-task-editor/add-new-cost-dlg/add-new-cost-dlg.component';
@NgModule({
declarations: [
WeekTaskEditorComponent,
HappinessWeekEditorComponent,
BestListDlgComponent,
HappinessWeekListDlgComponent,
BestEditorDlgComponent,
AddNewCostDlgComponent
],
imports: [
CommonModule,
FormsModule,
ThemeModule,
NbMenuModule,
NbInputModule,
NbCardModule,
NbButtonModule,
NbActionsModule,
NbCheckboxModule,
NbRadioModule,
NbDatepickerModule,
NbSelectModule,
NbIconModule,
NbTagModule,
NbStepperModule,
NbListModule,
NbUserModule,
NbSpinnerModule,
NbDialogModule,
NbAlertModule,
NbSpinnerModule,
AlertDlgModule,
FancyTableModule,
DropDownListModule,
NgxMaskModule,
CurrencyInputModule,
MaskDirectiveModule,
DateInputModule,
LoadingSpinnerModule
],
exports: [
BestListDlgComponent,
HappinessWeekListDlgComponent
]
})
export class HappinessModule { }

View File

@ -0,0 +1,34 @@
<nb-card status="success">
<nb-card-header>
新增開銷
</nb-card-header>
<nb-card-body>
<div class="row form-group">
<div class="col-6 col-md-4">
<div class='form-group'>
<label for='costTasker' class='label'>採購人</label>
<op-drop-down name="costTasker" [editable]="true" [(ngModel)]="task.tasker" [source]="taskrOptions">
</op-drop-down>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label for="costAmount" class="label">金額</label>
<input type="number" name="costAmount" nbInput fullWidth id="costAmount" [(ngModel)]="task.amount">
</div>
</div>
<div class="col-12 col-md-5">
<div class="form-group">
<label for="costContent" class="label">內容</label>
<input type="text" name="costContent" nbInput fullWidth id="costContent" [(ngModel)]="task.content">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()">Close</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small" (click)="submit()">Submit</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddNewCostDlgComponent } from './add-new-cost-dlg.component';
describe('AddNewCostDlgComponent', () => {
let component: AddNewCostDlgComponent;
let fixture: ComponentFixture<AddNewCostDlgComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AddNewCostDlgComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddNewCostDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { DropDownOption } from '../../../entity/dropDownOption';
import { HappinessCost } from '../../../entity/happiness-cost.model';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'ngx-add-new-cost-dlg',
templateUrl: './add-new-cost-dlg.component.html',
styleUrls: ['./add-new-cost-dlg.component.scss']
})
export class AddNewCostDlgComponent implements OnInit {
taskrOptions: DropDownOption[] = [];
task: HappinessCost;
constructor(
private authService: AuthService,
private dlgRef: NbDialogRef<AddNewCostDlgComponent>
) { }
ngOnInit(): void {
this.task = new HappinessCost();
this.task.tasker = this.authService.userAccess.firstName;
}
close() {
this.dlgRef.close();
}
submit() {
if (this.task.tasker && this.task.content && this.task.amount > 0) {
this.dlgRef.close(this.task);
}
}
}

View File

@ -0,0 +1,121 @@
<form #form="ngForm">
<nb-flip-card [showToggleButton]="false" [flipped]="showingCost">
<nb-card-front>
<nb-card>
<nb-card-header>
第 {{data.seq}} 周 {{data.topic}} 分工
<button class="float-right" nbButton hero status="success" size="small" (click)="toggleView()">
Cost
</button>
</nb-card-header>
<nb-card-body *ngIf="data">
<div class="row" *ngFor="let task of data.tasks;let i = index">
<div class="col-md-4">
<div class='form-group'>
<label for='tasker{{i}}' class='label'>{{getTaskTitle(task.type)}}</label>
<op-drop-down name="tasker{{i}}" [editable]="true" [(ngModel)]="task.tasker"
[source]="taskrOptions">
</op-drop-down>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label for="content{{i}}" class="label">內容</label>
<input type="text" name="content{{i}}" nbInput fullWidth id="content{{i}}"
[(ngModel)]="task.content">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="mr-2" nbButton hero status="info" size="small" (click)="gotoPreviousWeek()"
[nbSpinner]="processing">
Previous
</button>
<button nbButton hero status="info" size="small" (click)="gotoNextWeek()" [nbSpinner]="processing">
Next
</button>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Close
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid"
[nbSpinner]="processing">
Save
</button>
</nb-card-footer>
</nb-card>
</nb-card-front>
<nb-card-back>
<nb-card>
<nb-card-header>
第 {{data.seq}} 周 {{data.topic}} 開銷
<button class="float-right" nbButton hero status="success" size="small" (click)="toggleView()">
分工
</button>
<button class="float-right mr-1" nbButton hero status="success" size="small" (click)="addNewCost()">
新增開銷
</button>
</nb-card-header>
<nb-card-body *ngIf="data">
<div class="row" *ngFor="let task of data.costs;let i = index">
<div class="col-6 col-md-4">
<div class='form-group'>
<label for='costTasker{{i}}' class='label'>採購人</label>
<op-drop-down name="costTasker{{i}}" [editable]="true" [(ngModel)]="task.tasker"
[source]="taskrOptions">
</op-drop-down>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label for="costAmount{{i}}" class="label">金額</label>
<input type="number" name="costAmount{{i}}" nbInput fullWidth id="costAmount{{i}}"
[(ngModel)]="task.amount">
</div>
</div>
<div class="col-12 col-md-5">
<div class="form-group">
<label for="costContent{{i}}" class="label">內容</label>
<input type="text" name="costContent{{i}}" nbInput fullWidth id="costContent{{i}}"
[(ngModel)]="task.content">
</div>
</div>
</div>
<nb-alert status="primary" *ngIf="data.costs.length==0">
本周暫無開銷
</nb-alert>
</nb-card-body>
<nb-card-footer>
<button class="mr-2" nbButton hero status="info" size="small" (click)="gotoPreviousWeek()"
[nbSpinner]="processing">
Previous
</button>
<button nbButton hero status="info" size="small" (click)="gotoNextWeek()" [nbSpinner]="processing">
Next
</button>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Close
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!processing&&updateCost()" [nbSpinner]="processing">
Save
</button>
</nb-card-footer>
</nb-card>
</nb-card-back>
</nb-flip-card>
</form>

View File

@ -0,0 +1,3 @@
nb-flip-card{
max-width: 88vw;
}

View File

@ -0,0 +1,129 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { NbDialogRef, NbDialogService, NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../entity/dropDownOption';
import { HappinessCost } from '../../entity/happiness-cost.model';
import { HappinessWeek, HappinessTaskType, HappinessTask } from '../../entity/HappinessGroup';
import { HappinessCostService } from '../../services/crudServices/happiness-cost.service';
import { HappinessTaskService } from '../../services/crudServices/happiness-group.service';
import { StringUtils } from '../../utilities/string-utils';
import { AddNewCostDlgComponent } from './add-new-cost-dlg/add-new-cost-dlg.component';
@Component({
selector: 'ngx-week-task-editor',
templateUrl: './week-task-editor.component.html',
styleUrls: ['./week-task-editor.component.scss']
})
export class WeekTaskEditorComponent implements OnInit {
processing: boolean = false;
allData: HappinessWeek[];
data: HappinessWeek;
taskrOptions: DropDownOption[] = [];
showingCost: boolean = false;
constructor(
private happinessTaskService: HappinessTaskService,
private happinessCostService: HappinessCostService,
private dlgRef: NbDialogRef<WeekTaskEditorComponent>,
private dlgService: NbDialogService,
private toastrService: NbToastrService,
private cdRef: ChangeDetectorRef
) {
}
ngOnInit(): void {
if (this.data.tasks.length == 0) {
this.data.tasks = [];
for (let i = HappinessTaskType.IceBreak; i <= HappinessTaskType.Dessert; i++) {
this.data.tasks.push(
{
weekId: this.data.id,
id: 'new',
type: i as HappinessTaskType,
tasker: '',
content: ''
} as HappinessTask
)
}
}
let names = [] as string[];
this.allData.forEach(week => {
if (week.tasks) {
names = names.concat(week.tasks.map(t => t.tasker ? t.tasker.trim() : ''));
}
});
this.taskrOptions = names.filter(function (item, pos) {
return !StringUtils.isNullOrWhitespace(item) && names.indexOf(item) == pos;
}).map(name => new DropDownOption(name, name));
}
close() {
this.dlgRef.close();
}
update() {
if (this.processing == false) {
this.processing = true;
this.happinessTaskService.createOrUpdateAll(this.data.tasks).pipe(first()).subscribe(result => {
this.processing = false;
this.toastrService.success("Update Success!");
});
}
}
updateCost() {
if (this.processing == false) {
this.processing = true;
this.happinessCostService.createOrUpdateAll(this.data.costs).pipe(first()).subscribe(result => {
this.processing = false;
this.toastrService.success("Update Cost Success!");
});
}
}
getTaskTitle(t: HappinessTaskType) {
switch (t) {
case HappinessTaskType.IceBreak: return '帶遊戲';
case HappinessTaskType.Worship: return '唱歌';
case HappinessTaskType.Testimony: return '見證';
case HappinessTaskType.Message: return '信息';
case HappinessTaskType.Gift: return '準備禮物';
case HappinessTaskType.Dessert: return '準備點心';
default: break;
}
}
toggleView() {
this.showingCost = !this.showingCost;
}
addNewCost() {
this.dlgService.open(AddNewCostDlgComponent, {
context: {
taskrOptions: this.taskrOptions
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
result.weekId = this.data.id;
this.happinessCostService.createOrUpdate(result).pipe(first()).subscribe(result => {
this.data.costs.push(result);
});
}
});
}
gotoNextWeek() {
if ((this.allData.indexOf(this.data) + 1) < this.allData.length) {
this.data = this.allData[this.allData.indexOf(this.data) + 1];
this.cdRef.detectChanges();
}
}
gotoPreviousWeek() {
if ((this.allData.indexOf(this.data) - 1) >= 0) {
this.data = this.allData[this.allData.indexOf(this.data) - 1];
this.cdRef.detectChanges();
}
}
}

View File

@ -5,7 +5,7 @@
data-header-fix-moment-classes="light-theme u-theme-shadow-v1 g-bg-white g-py-5--md">
<nav class="navbar navbar-expand-lg p-0 g-px-15">
<div class="mx-auto">
<a href="/CellGroup/prayer" class="g-hidden-lg-up navbar-brand mr-0">
<a href="/myapp" class="g-hidden-lg-up navbar-brand mr-0">
<img class="d-block g-height-50 g-height-60--md" src="/assets/home/images/logo-light.png"
alt="Image Description" data-header-fix-moment-exclude="d-block"
data-header-fix-moment-classes="d-none">
@ -29,7 +29,7 @@
</li>
<!-- Logo -->
<li class="g-hidden-lg-down nav-logo-item g-mx-15--lg">
<a href="#hometop" class="js-go-to navbar-brand mr-0" data-type="static">
<a href="/myapp" class="js-go-to navbar-brand mr-0" data-type="static">
<img class="d-block g-height-50 g-height-60--md"
src="/assets/home/images/logo-light.png" alt="Image Description"
data-header-fix-moment-exclude="d-block" data-header-fix-moment-classes="d-none">

View File

@ -155,10 +155,11 @@ export class InvitationComponent implements OnInit {
.type('這周想邀請你一起來度過一個輕鬆舒壓的夜晚!')
break;
case 8:
instance.type(`Hi, <strong>${this.best.name}</strong>,<br>`)
.type('你有多久沒有和家人朋友一同野餐了呢?<br>')
.type('這周六想邀請你與我們沐浴陽光,因為<b>天父爸爸</b>說:')
.options({ speed: 350 }).type('<h3>歡迎回家!</h3>')
instance.type(`Hi, 親愛的<strong>${this.best.name}</strong>,<br>`)
.type('經歷了這幾週的幸福之旅,<br>')
.type('在這週我們想要邀請你來更認識我們充滿愛的 <strong>家</strong><br> ')
.type('和我們度過一段溫馨的家庭聚會時光, <br>')
.type('感受屬於家的溫暖和幸福!')
break;
default:

View File

@ -10,6 +10,7 @@ import { Subject } from 'rxjs';
import { first, takeUntil } from 'rxjs/operators';
import { LoginTokenViewModel, RegisterViewModel } from '../entity/Auth';
import { AuthService } from '../services/auth.service';
import { PastoralDomainService } from '../services/crudServices/pastoral-domain.service';
import { SessionService } from '../services/session.service';
@Component({
@ -40,7 +41,9 @@ export class LoginComponent {
protected router: Router,
private route: ActivatedRoute,
private sessionService: SessionService,
private authService: AuthService) {
private authService: AuthService,
private pastoralDomainService: PastoralDomainService
) {
this.redirectDelay = this.getConfigValue('forms.login.redirectDelay');
this.showMessages = this.getConfigValue('forms.login.showMessages');
@ -94,11 +97,13 @@ export class LoginComponent {
if (result && result.token) {
if (this.sessionService.redirectAfterLogin) {
setTimeout(() => {
return this.router.navigateByUrl(this.sessionService.redirectAfterLogin);
}, this.redirectDelay);
}
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
if (this.sessionService.redirectAfterLogin) {
setTimeout(() => {
return this.router.navigateByUrl(this.sessionService.redirectAfterLogin);
}, this.redirectDelay);
}
});
} else {
this.errors = ['Invalid Credential.'];
}
@ -124,7 +129,9 @@ export class LoginComponent {
this.authService.loginWithGoogleAccessToken(authResult.getToken().getValue())
.pipe(first()).subscribe(result => {
if (result) {
this.redirect();
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
this.redirect();
});
} else {
this.isCallback = false;
}

View File

@ -79,7 +79,9 @@ export class ApiPrefixInterceptor implements HttpInterceptor {
if (value instanceof Object) {
this.convertDates(value);
}
if (typeof value === 'string' &&
if (
(typeof value === 'string' || typeof value === 'undefined')
&&
(key.toLowerCase().indexOf('date') > -1 || key.toLowerCase().indexOf('time') > -1 || key.toLowerCase().indexOf('birthday') > -1)
) {
//object[key] = new Date(value.replace('+00:00', ' UTC'));

View File

@ -6,6 +6,7 @@ import { Observable } from 'rxjs';
import { map, mergeMap, first } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { GoogleUserInfo, LoginTokenViewModel, RegisterViewModel } from '../entity/Auth';
import { PastoralDomainService } from './crudServices/pastoral-domain.service';
import { SessionService } from './session.service';
@Injectable({
@ -20,12 +21,13 @@ export class AuthService {
private sessionService: SessionService,
private oAuthService: NbAuthService,
protected router: Router,
private pastoralDomainService: PastoralDomainService,
) { }
logout() {
this.sessionService.loginUserToken = '';
this.userAccess = {} as LoginTokenViewModel;
this.pastoralDomainService.domains = [];
this.oAuthService.getToken().pipe(first()).subscribe(result => {
console.log('logoutToken', result);
if (result.isValid() && result.getOwnerStrategyName()) {
@ -48,6 +50,11 @@ export class AuthService {
return this.loginWithToken(this.sessionService.loginUserToken).pipe(map(result => {
if (!result) {
this.sessionService.loginUserToken = "";
} else {
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
});
}
return result;
}));;
@ -66,7 +73,7 @@ export class AuthService {
}
private headerObj = new HttpHeaders({ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36' })
loginWithGoogleAccessToken(googleToken) {
loginWithGoogleAccessToken(googleToken): Observable<LoginTokenViewModel> {
return this.post('auth/oauth-login',
{
@ -75,26 +82,26 @@ export class AuthService {
} as RegisterViewModel,
);
return this.http.get(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${googleToken}`, {
headers: this.headerObj
})
.pipe(
map(googleUserInfo => { return googleUserInfo as GoogleUserInfo }),
mergeMap(
googleUserInfo => {
if (!googleUserInfo.error) {
// return this.http.get(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${googleToken}`, {
// headers: this.headerObj
// })
// .pipe(
// map(googleUserInfo => { return googleUserInfo as GoogleUserInfo }),
// mergeMap(
// googleUserInfo => {
// if (!googleUserInfo.error) {
return this.post('auth/sign-up',
{
email: googleUserInfo.email,
fistName: googleUserInfo.given_name,
lastName: googleUserInfo.family_name,
avatarImage: googleUserInfo.picture
} as RegisterViewModel,
);
}
return Observable.of(null);
}));
// return this.post('auth/sign-up',
// {
// email: googleUserInfo.email,
// fistName: googleUserInfo.given_name,
// lastName: googleUserInfo.family_name,
// avatarImage: googleUserInfo.picture
// } as RegisterViewModel,
// );
// }
// return Observable.of(null);
// }));
}
@ -154,6 +161,7 @@ export class AuthService {
if (result && result.token) {
this.userAccess = result;
this.sessionService.loginUserToken = result.token;
this.sessionService.loginUserId = result.memberId;
} else {
this.userAccess = {} as LoginTokenViewModel;
}

View File

@ -1,9 +1,21 @@
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { PastoralDomain } from '../../entity/PastoralDomain';
import { PastoralDomainService } from './pastoral-domain.service';
@Injectable({
providedIn: 'root'
})
export class CellGroupService {
constructor() { }
cellGroup: PastoralDomain = null;
constructor(private pastoralDomainService: PastoralDomainService) {
}
initialize() {
return this.pastoralDomainService.getCurrentUserPastoralDomain().pipe(map(result => {
this.cellGroup
return result;
}));
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ContributionService } from './contribution.service';
describe('ContributionService', () => {
let service: ContributionService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ContributionService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { CrudService } from './crud.service';
import { first, map } from 'rxjs/operators';
import { Contribution } from '../../entity/contribution.model';
const BASE_URL = (action: string = null) => { return `Contribution${(action ? `/${action}` : '')}` }
@Injectable({
providedIn: 'root'
})
export class ContributionService extends CrudService<Contribution> {
constructor(protected http: HttpClient) {
super(http, BASE_URL);
}
}

View File

@ -11,8 +11,8 @@ export interface ICrudService<T> {
getAll(): Observable<T[]>
getById(id: any): Observable<T>;
update(data: T): Observable<boolean>;
createOrUpdate(data: T): Observable<boolean>;
createOrUpdateAll(data: T[]): Observable<boolean>;
createOrUpdate(data: T): Observable<T>;
createOrUpdateAll(data: T[]): Observable<T[]>;
delete(id: any): Observable<T>;
}
@ -51,7 +51,7 @@ export class CrudService<T> implements ICrudService<T> {
}
createOrUpdate(data: T) {
return this.http.post<boolean>(this.POST_URL_CREATE_OR_UPDATE(),
return this.http.post<T>(this.POST_URL_CREATE_OR_UPDATE(),
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
@ -61,7 +61,7 @@ export class CrudService<T> implements ICrudService<T> {
}
createOrUpdateAll(data: T[]) {
return this.http.post<boolean>(this.POST_URL_CREATE_OR_UPDATE_ALL(),
return this.http.post<T[]>(this.POST_URL_CREATE_OR_UPDATE_ALL(),
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
@ -133,7 +133,7 @@ export class CombinedKeyCrudService<T> implements ICrudService<T> {
}
createOrUpdate(data: T) {
return this.http.post<boolean>(this.POST_URL_CREATE_OR_UPDATE(),
return this.http.post<T>(this.POST_URL_CREATE_OR_UPDATE(),
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
@ -143,7 +143,7 @@ export class CombinedKeyCrudService<T> implements ICrudService<T> {
}
createOrUpdateAll(data: T[]) {
return this.http.post<boolean>(this.POST_URL_CREATE_OR_UPDATE_ALL(),
return this.http.post<T[]>(this.POST_URL_CREATE_OR_UPDATE_ALL(),
JSON.stringify(data), {
headers: {
'Accept': 'application/json',

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DomainMemberShipService } from './domain-member-ship.service';
describe('DomainMemberShipService', () => {
let service: DomainMemberShipService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DomainMemberShipService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,52 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { NewVisitor } from '../../entity/NewVisitor';
import { CrudService } from './crud.service';
import { first, map } from 'rxjs/operators';
import { DomainMemberRelationship } from '../../entity/PastoralDomain';
const BASE_URL = (action: string = null) => { return `DomainMemberShip${(action ? `/${action}` : '')}` }
const POST_URL_ASSIGN_CELL_GROUPS = 'DomainMemberShip/AssignCellGroups'
const GET_URL_ADD_MEMBER_INTO_GROUP = (domainId, memberId) => `DomainMemberShip/AddMemberIntoGroup?domainId=${domainId}&memberId=${memberId}`;
const GET_URL_REMOVE_MEMBER_FROM_GROUP = (domainId, memberId) => `DomainMemberShip/RemoveMemberFromGroup?domainId=${domainId}&memberId=${memberId}`;
const POST_URL_UPDATE_MEMBERS_IN_GROUP = 'DomainMemberShip/UpdateMembersInGroup'
@Injectable({
providedIn: 'root'
})
export class DomainMemberShipService extends CrudService<DomainMemberRelationship> {
constructor(protected http: HttpClient) {
super(http, BASE_URL);
}
assignCellGroups(data) {
return this.http.post<boolean>(POST_URL_ASSIGN_CELL_GROUPS,
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
}
addMemberIntoGroup(domainId: string, memberId: string) {
return this.http.get<number>(GET_URL_ADD_MEMBER_INTO_GROUP(domainId, memberId));
}
removeMemberFromGroup(domainId: string, memberId: string) {
return this.http.get<number>(GET_URL_REMOVE_MEMBER_FROM_GROUP(domainId, memberId));
}
updateMembersInGroup(data) {
return this.http.post<boolean>(POST_URL_UPDATE_MEMBERS_IN_GROUP,
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { HappinessCostService } from './happiness-cost.service';
describe('HappinessCostService', () => {
let service: HappinessCostService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HappinessCostService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { CrudService } from './crud.service';
import { first, map } from 'rxjs/operators';
import { HappinessCost } from '../../entity/happiness-cost.model';
const BASE_URL = (action: string = null) => { return `HappinessCost${(action ? `/${action}` : '')}` }
@Injectable({
providedIn: 'root'
})
export class HappinessCostService extends CrudService<HappinessCost> {
constructor(protected http: HttpClient) {
super(http, BASE_URL);
}
}

View File

@ -1,40 +1,66 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DomainMemberRelationship, PastoralDomain } from '../../entity/PastoralDomain';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { DomainMemberRelationship, DomainType, PastoralDomain } from '../../entity/PastoralDomain';
import { AuthService } from '../auth.service';
import { SessionService } from '../session.service';
import { CombinedKeyCrudService, CrudService } from './crud.service';
import { DomainMemberShipService } from './domain-member-ship.service';
const BASE_URL = (action: string = null) => { return `PastoralDomain${(action ? `/${action}` : '')}` }
const RELATIONSHIP_BASE_URL = (action: string = null) => { return `DomainMemberShip${(action ? `/${action}` : '')}` }
const POST_URL_ASSIGN_CELL_GROUPS = 'DomainMemberShip/AssignCellGroups'
const GET_URL_GET_CURRENT_USER_PASTORAL_DOMAIN = `PastoralDomain/GetCurrentUserPastoralDomain`;
@Injectable({
providedIn: 'root'
})
export class PastoralDomainService extends CrudService<PastoralDomain> {
constructor(protected http: HttpClient) {
initialized = new BehaviorSubject<boolean>(false);
domains: PastoralDomain[] = [];
constructor(
protected http: HttpClient,
private domainMemberShipService: DomainMemberShipService,
private session: SessionService
) {
super(http, BASE_URL);
}
}
@Injectable({
providedIn: 'root'
})
export class DomainMemberShipService extends CombinedKeyCrudService<DomainMemberRelationship> {
constructor(protected http: HttpClient) {
super(http, RELATIONSHIP_BASE_URL);
}
assignCellGroups(data: DomainMemberRelationship[]) {
return this.http.post<boolean>(POST_URL_ASSIGN_CELL_GROUPS,
JSON.stringify(data), {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
getCurrentUserPastoralDomain() {
return this.http.get<PastoralDomain[]>(GET_URL_GET_CURRENT_USER_PASTORAL_DOMAIN);
}
initialize() {
return this.getCurrentUserPastoralDomain().pipe(map(result => {
this.domains = result;
this.initialized.next(true);
return result;
}));
}
public get myCellGroup(): PastoralDomain {
return this.domains.find(d => d.type == DomainType.CellGroup);
}
public get myHappinessGroup(): PastoralDomain {
return this.domains.find(d => d.type == DomainType.HappinessGroup);
}
joinDomain(domainId: string) {
return this.domainMemberShipService.addMemberIntoGroup(domainId, this.session.loginUserId).pipe(
map(firstResult => { return firstResult }),
mergeMap(
firstResult => {
if (firstResult) {
return this.initialize();
} else {
return Observable.of(this.domains);
}
}));
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { FileService } from './file.service';
describe('FileService', () => {
let service: FileService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FileService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
const FilePath_URL = (id: string = null) => { return `${environment.apiUrl}/Files?filePath=${(id ? `/${id}` : '')}` }
@Injectable({
providedIn: 'root'
})
export class FileService {
constructor() { }
ImageUrl(imagePath: string) {
return FilePath_URL(imagePath);
}
}

Some files were not shown because too many files have changed in this diff Show More