@luckydye/calendar 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,73 +1,73 @@
1
- import { css, html, LitElement } from "lit";
1
+ import { LitElement, css, html } from "lit";
2
+ import { CalDAVSource as CalDAVSourceClass } from "./CalDAVSource.js";
2
3
  import type { CalendarSource } from "./CalendarIntegration.js";
3
4
  import { authenticateWithGoogle } from "./GoogleCalendarSource.js";
4
5
  import { InhouseBookingSource } from "./InhouseBookingSource.js";
5
- import { CalDAVSource as CalDAVSourceClass } from "./CalDAVSource.js";
6
6
 
7
7
  interface CalDAVSourceConfig extends CalendarSource {
8
- type: "caldav";
9
- credentials: {
10
- serverUrl: string;
11
- username: string;
12
- password: string;
13
- };
14
- locked?: boolean;
8
+ type: "caldav";
9
+ credentials: {
10
+ serverUrl: string;
11
+ username: string;
12
+ password: string;
13
+ };
14
+ locked?: boolean;
15
15
  }
16
16
 
17
17
  interface ICalSource extends CalendarSource {
18
- type: "ical";
19
- credentials: {
20
- url: string;
21
- };
22
- locked?: boolean;
18
+ type: "ical";
19
+ credentials: {
20
+ url: string;
21
+ };
22
+ locked?: boolean;
23
23
  }
24
24
 
25
25
  interface GoogleSource extends CalendarSource {
26
- type: "google";
27
- credentials: {
28
- accessToken: string;
29
- refreshToken?: string;
30
- tokenExpiry?: string;
31
- calendarId?: string;
32
- };
33
- locked?: boolean;
26
+ type: "google";
27
+ credentials: {
28
+ accessToken: string;
29
+ refreshToken?: string;
30
+ tokenExpiry?: string;
31
+ calendarId?: string;
32
+ };
33
+ locked?: boolean;
34
34
  }
35
35
 
36
36
  interface InhouseSource extends CalendarSource {
37
- type: "inhouse";
38
- credentials: {
39
- sessionCookie: string;
40
- employeeId: string;
41
- unitId?: string;
42
- startHour?: 9 | 10;
43
- };
44
- locked?: boolean;
37
+ type: "inhouse";
38
+ credentials: {
39
+ sessionCookie: string;
40
+ employeeId: string;
41
+ unitId?: string;
42
+ startHour?: 9 | 10;
43
+ };
44
+ locked?: boolean;
45
45
  }
46
46
 
47
47
  interface TimeseriesJsonSource extends CalendarSource {
48
- type: "timeseries-json";
49
- credentials: {
50
- url: string;
51
- };
52
- locked?: boolean;
48
+ type: "timeseries-json";
49
+ credentials: {
50
+ url: string;
51
+ };
52
+ locked?: boolean;
53
53
  }
54
54
 
55
55
  type ConfigurableSource =
56
- | CalDAVSourceConfig
57
- | ICalSource
58
- | GoogleSource
59
- | InhouseSource
60
- | TimeseriesJsonSource;
56
+ | CalDAVSourceConfig
57
+ | ICalSource
58
+ | GoogleSource
59
+ | InhouseSource
60
+ | TimeseriesJsonSource;
61
61
 
62
62
  interface SidebarCalendar {
63
- id: string;
64
- name: string;
65
- color: string;
66
- sourceId: string;
63
+ id: string;
64
+ name: string;
65
+ color: string;
66
+ sourceId: string;
67
67
  }
68
68
 
69
69
  export class CalDAVConfigElement extends LitElement {
70
- static styles = css`
70
+ static styles = css`
71
71
  :host {
72
72
  display: block;
73
73
  font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
@@ -457,405 +457,438 @@ export class CalDAVConfigElement extends LitElement {
457
457
  }
458
458
  `;
459
459
 
460
- sources: ConfigurableSource[] = [];
461
- calendars: SidebarCalendar[] = [];
462
- activeCalendarId: string | null = null;
463
- isAdding = false;
464
- editingId: string | null = null;
465
- isGoogleAuthenticating = false;
466
- googleAuthError: string | null = null;
467
- collapsed = localStorage.getItem("caldav-sidebar-collapsed") === "true";
468
-
469
- private formData: Partial<ConfigurableSource> = {};
470
-
471
- static get properties() {
472
- return {
473
- sources: { type: Array },
474
- calendars: { type: Array },
475
- activeCalendarId: { type: String },
476
- isAdding: { type: Boolean },
477
- editingId: { type: String },
478
- isGoogleAuthenticating: { type: Boolean },
479
- googleAuthError: { type: String },
480
- collapsed: { type: Boolean, reflect: true },
481
- };
482
- }
483
-
484
- constructor() {
485
- super();
486
- this.loadSources();
487
-
488
- // Check for OAuth callback (authorization code flow)
489
- if (typeof window !== 'undefined' && window.location.search) {
490
- const params = new URLSearchParams(window.location.search);
491
- const code = params.get('code');
492
- const error = params.get('error');
493
- const state = params.get('state');
494
-
495
- if (code || error) {
496
- // Send the result to the opener window
497
- if (window.opener) {
498
- window.opener.postMessage({
499
- type: 'google-oauth-callback',
500
- code: code,
501
- error: error,
502
- receivedState: state,
503
- }, window.location.origin);
504
- window.close();
505
- }
506
- }
507
- }
508
- }
509
-
510
- connectedCallback() {
511
- super.connectedCallback();
512
- this.loadSources();
513
- }
514
-
515
- loadSources() {
516
- const saved = localStorage.getItem("caldav-sources");
517
- if (saved) {
518
- try {
519
- this.sources = JSON.parse(saved);
520
- } catch {
521
- this.sources = [];
522
- }
523
- }
524
-
525
- }
526
-
527
- saveSources() {
528
- localStorage.setItem("caldav-sources", JSON.stringify(this.sources));
529
- this.dispatchEvent(new CustomEvent("sources-changed", {
530
- detail: { sources: this.sources },
531
- bubbles: true,
532
- }));
533
- this.requestUpdate();
534
- }
535
-
536
- toggleCollapsed() {
537
- this.collapsed = !this.collapsed;
538
- localStorage.setItem("caldav-sidebar-collapsed", String(this.collapsed));
539
- this.dispatchEvent(new CustomEvent("collapsed-changed", {
540
- detail: { collapsed: this.collapsed },
541
- bubbles: true,
542
- }));
543
- }
544
-
545
- addSource() {
546
- this.isAdding = true;
547
- this.editingId = null;
548
- this.formData = {
549
- type: "caldav",
550
- color: "#FF6E68",
551
- enabled: true,
552
- };
553
- this.requestUpdate();
554
- }
555
-
556
- editSource(source: ConfigurableSource) {
557
- this.isAdding = false;
558
- this.editingId = source.id;
559
- this.formData = { ...source };
560
- this.requestUpdate();
561
- }
562
-
563
- deleteSource(id: string) {
564
- this.sources = this.sources.filter((s) => s.id !== id);
565
- this.saveSources();
566
- }
567
-
568
- toggleEnabled(source: ConfigurableSource) {
569
- source.enabled = !source.enabled;
570
- this.saveSources();
571
- }
572
-
573
- toggleLocked(source: ConfigurableSource) {
574
- source.locked = !source.locked;
575
- this.saveSources();
576
- }
577
-
578
- selectCalendar(calendarId: string) {
579
- this.activeCalendarId = calendarId;
580
- this.dispatchEvent(new CustomEvent("active-calendar-changed", {
581
- detail: { calendarId },
582
- bubbles: true,
583
- }));
584
- }
585
-
586
- saveForm() {
587
- if (!this.formData.name?.trim()) {
588
- alert('Please enter a calendar name');
589
- return;
590
- }
591
-
592
- if (!this.formData.type) {
593
- alert('Please select a calendar type');
594
- return;
595
- }
596
-
597
- let source: ConfigurableSource;
598
-
599
- if (this.formData.type === "caldav") {
600
- if (
601
- !this.formData.credentials?.serverUrl ||
602
- !this.formData.credentials?.username ||
603
- !this.formData.credentials?.password
604
- ) {
605
- return;
606
- }
607
-
608
- source = {
609
- id: this.editingId || crypto.randomUUID(),
610
- name: this.formData.name,
611
- type: "caldav",
612
- credentials: {
613
- serverUrl: this.formData.credentials.serverUrl,
614
- username: this.formData.credentials.username,
615
- password: this.formData.credentials.password,
616
- },
617
- color: this.formData.color || "#FF6E68",
618
- enabled: this.formData.enabled ?? true,
619
- locked: this.formData.locked ?? false,
620
- } as CalDAVSourceConfig;
621
- } else if (this.formData.type === "ical") {
622
- if (!this.formData.credentials?.url) {
623
- return;
624
- }
625
-
626
- source = {
627
- id: this.editingId || crypto.randomUUID(),
628
- name: this.formData.name,
629
- type: "ical",
630
- credentials: {
631
- url: this.formData.credentials.url,
632
- },
633
- color: this.formData.color || "#FF6E68",
634
- enabled: this.formData.enabled ?? true,
635
- locked: this.formData.locked ?? false,
636
- } as ICalSource;
637
- } else if (this.formData.type === "timeseries-json") {
638
- if (!this.formData.credentials?.url) {
639
- return;
640
- }
641
-
642
- source = {
643
- id: this.editingId || crypto.randomUUID(),
644
- name: this.formData.name,
645
- type: "timeseries-json",
646
- credentials: {
647
- url: this.formData.credentials.url,
648
- },
649
- color: this.formData.color || "#06B6D4",
650
- enabled: this.formData.enabled ?? true,
651
- locked: this.formData.locked ?? false,
652
- } as TimeseriesJsonSource;
653
- } else if (this.formData.type === "google") {
654
- if (!this.formData.credentials?.accessToken) {
655
- alert('Please authenticate with Google before adding the calendar');
656
- return;
657
- }
658
-
659
- source = {
660
- id: this.editingId || crypto.randomUUID(),
661
- name: this.formData.name,
662
- type: "google",
663
- credentials: {
664
- accessToken: this.formData.credentials.accessToken,
665
- refreshToken: this.formData.credentials.refreshToken,
666
- tokenExpiry: this.formData.credentials.tokenExpiry,
667
- calendarId: this.formData.credentials.calendarId || "primary",
668
- },
669
- color: this.formData.color || "#4285F4",
670
- enabled: this.formData.enabled ?? true,
671
- locked: this.formData.locked ?? false,
672
- } as GoogleSource;
673
- } else if (this.formData.type === "inhouse") {
674
- if (!this.formData.credentials?.sessionCookie || !this.formData.credentials?.employeeId) {
675
- alert('Please enter session cookie and employee ID');
676
- return;
677
- }
678
-
679
- source = {
680
- id: this.editingId || crypto.randomUUID(),
681
- name: this.formData.name,
682
- type: "inhouse",
683
- credentials: {
684
- sessionCookie: this.formData.credentials.sessionCookie,
685
- employeeId: this.formData.credentials.employeeId,
686
- unitId: this.formData.credentials.unitId,
687
- startHour: this.formData.credentials.startHour,
688
- },
689
- color: this.formData.color || "#FF6E68",
690
- enabled: this.formData.enabled ?? true,
691
- locked: this.formData.locked ?? false,
692
- } as InhouseSource;
693
- } else {
694
- return;
695
- }
696
-
697
- if (this.editingId) {
698
- const index = this.sources.findIndex((s) => s.id === this.editingId);
699
- if (index >= 0) {
700
- this.sources[index] = source;
701
- }
702
- } else {
703
- this.sources.push(source);
704
- }
705
-
706
- this.isAdding = false;
707
- this.editingId = null;
708
- this.formData = {};
709
- this.googleAuthError = null;
710
- this.saveSources();
711
- this.requestUpdate();
712
- }
713
-
714
- cancelForm() {
715
- this.isAdding = false;
716
- this.editingId = null;
717
- this.formData = {};
718
- this.googleAuthError = null;
719
- this.isGoogleAuthenticating = false;
720
- this.requestUpdate();
721
- }
722
-
723
- updateForm(field: string, value: string | boolean | number) {
724
- if (field === "serverUrl" || field === "username" || field === "password" || field === "url" ||
725
- field === "accessToken" || field === "refreshToken" || field === "tokenExpiry" || field === "calendarId" ||
726
- field === "sessionCookie" || field === "employeeId" || field === "unitId" || field === "startHour") {
727
- this.formData = {
728
- ...this.formData,
729
- credentials: {
730
- ...this.formData.credentials,
731
- [field]: value,
732
- } as ConfigurableSource["credentials"],
733
- };
734
- } else {
735
- this.formData = { ...this.formData, [field]: value };
736
- }
737
- this.requestUpdate();
738
- }
739
-
740
- async signInWithGoogle() {
741
- this.isGoogleAuthenticating = true;
742
- this.googleAuthError = null;
743
- this.requestUpdate();
744
-
745
- try {
746
- // Load Google OAuth credentials from public directory
747
- const credentialsResponse = await fetch('/credentials_google.json');
748
- if (!credentialsResponse.ok) {
749
- throw new Error('Google OAuth credentials file not found. Please add credentials_google.json to the public directory.');
750
- }
751
-
752
- const credentials = await credentialsResponse.json();
753
- const clientId = credentials.installed?.client_id || credentials.web?.client_id;
754
- const clientSecret = credentials.installed?.client_secret || credentials.web?.client_secret;
755
-
756
- if (!clientId || !clientSecret) {
757
- throw new Error('Invalid credentials file format. Expected client_id and client_secret.');
758
- }
759
-
760
- const tokens = await authenticateWithGoogle(clientId, clientSecret);
761
-
762
- // Fetch the list of calendars from Google
763
- const calendarListResponse = await fetch(
764
- 'https://www.googleapis.com/calendar/v3/users/me/calendarList',
765
- {
766
- headers: {
767
- Authorization: `Bearer ${tokens.accessToken}`,
768
- },
769
- }
770
- );
771
-
772
- if (!calendarListResponse.ok) {
773
- throw new Error('Failed to fetch calendar list from Google');
774
- }
775
-
776
- const calendarList = await calendarListResponse.json();
777
-
778
- // Create a source for each calendar
779
- for (const calendar of calendarList.items || []) {
780
- const newSource: GoogleSource = {
781
- id: crypto.randomUUID(),
782
- name: calendar.summary || 'Untitled Calendar',
783
- type: 'google',
784
- credentials: {
785
- accessToken: tokens.accessToken,
786
- refreshToken: tokens.refreshToken,
787
- tokenExpiry: tokens.expiry,
788
- calendarId: calendar.id,
789
- },
790
- color: calendar.backgroundColor || '#4285F4',
791
- enabled: true,
792
- };
793
-
794
- this.sources.push(newSource);
795
- }
796
-
797
- this.saveSources();
798
- this.isAdding = false;
799
- this.formData = {};
800
- this.isGoogleAuthenticating = false;
801
- this.requestUpdate();
802
- } catch (error) {
803
- this.isGoogleAuthenticating = false;
804
- this.googleAuthError = error instanceof Error ? error.message : "Authentication failed";
805
- this.requestUpdate();
806
- }
807
- }
808
-
809
- close() {
810
- this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
811
- }
812
-
813
- render() {
814
- const colors = [
815
- "#FF6E68",
816
- "#FFB800",
817
- "#83D754",
818
- "#0095FD",
819
- "#9FC6E7",
820
- "#888082",
821
- "#E91E63",
822
- "#9C27B0",
823
- "#673AB7",
824
- "#3F51B5",
825
- "#2196F3",
826
- "#00BCD4",
827
- "#009688",
828
- "#4CAF50",
829
- "#8BC34A",
830
- "#CDDC39",
831
- "#FFEB3B",
832
- "#FFC107",
833
- "#FF9800",
834
- "#FF5722",
835
- "#795548",
836
- ];
837
-
838
- return html`
460
+ sources: ConfigurableSource[] = [];
461
+ calendars: SidebarCalendar[] = [];
462
+ activeCalendarId: string | null = null;
463
+ isAdding = false;
464
+ editingId: string | null = null;
465
+ isGoogleAuthenticating = false;
466
+ googleAuthError: string | null = null;
467
+ collapsed = localStorage.getItem("caldav-sidebar-collapsed") === "true";
468
+
469
+ private formData: Partial<ConfigurableSource> = {};
470
+
471
+ static get properties() {
472
+ return {
473
+ sources: { type: Array },
474
+ calendars: { type: Array },
475
+ activeCalendarId: { type: String },
476
+ isAdding: { type: Boolean },
477
+ editingId: { type: String },
478
+ isGoogleAuthenticating: { type: Boolean },
479
+ googleAuthError: { type: String },
480
+ collapsed: { type: Boolean, reflect: true },
481
+ };
482
+ }
483
+
484
+ constructor() {
485
+ super();
486
+ this.loadSources();
487
+
488
+ // Check for OAuth callback (authorization code flow)
489
+ if (typeof window !== "undefined" && window.location.search) {
490
+ const params = new URLSearchParams(window.location.search);
491
+ const code = params.get("code");
492
+ const error = params.get("error");
493
+ const state = params.get("state");
494
+
495
+ if (code || error) {
496
+ // Send the result to the opener window
497
+ if (window.opener) {
498
+ window.opener.postMessage(
499
+ {
500
+ type: "google-oauth-callback",
501
+ code: code,
502
+ error: error,
503
+ receivedState: state,
504
+ },
505
+ window.location.origin,
506
+ );
507
+ window.close();
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ connectedCallback() {
514
+ super.connectedCallback();
515
+ this.loadSources();
516
+ }
517
+
518
+ loadSources() {
519
+ const saved = localStorage.getItem("caldav-sources");
520
+ if (saved) {
521
+ try {
522
+ this.sources = JSON.parse(saved);
523
+ } catch {
524
+ this.sources = [];
525
+ }
526
+ }
527
+ }
528
+
529
+ saveSources() {
530
+ localStorage.setItem("caldav-sources", JSON.stringify(this.sources));
531
+ this.dispatchEvent(
532
+ new CustomEvent("sources-changed", {
533
+ detail: { sources: this.sources },
534
+ bubbles: true,
535
+ }),
536
+ );
537
+ this.requestUpdate();
538
+ }
539
+
540
+ toggleCollapsed() {
541
+ this.collapsed = !this.collapsed;
542
+ localStorage.setItem("caldav-sidebar-collapsed", String(this.collapsed));
543
+ this.dispatchEvent(
544
+ new CustomEvent("collapsed-changed", {
545
+ detail: { collapsed: this.collapsed },
546
+ bubbles: true,
547
+ }),
548
+ );
549
+ }
550
+
551
+ addSource() {
552
+ this.isAdding = true;
553
+ this.editingId = null;
554
+ this.formData = {
555
+ type: "caldav",
556
+ color: "#FF6E68",
557
+ enabled: true,
558
+ };
559
+ this.requestUpdate();
560
+ }
561
+
562
+ editSource(source: ConfigurableSource) {
563
+ this.isAdding = false;
564
+ this.editingId = source.id;
565
+ this.formData = { ...source };
566
+ this.requestUpdate();
567
+ }
568
+
569
+ deleteSource(id: string) {
570
+ this.sources = this.sources.filter((s) => s.id !== id);
571
+ this.saveSources();
572
+ }
573
+
574
+ toggleEnabled(source: ConfigurableSource) {
575
+ source.enabled = !source.enabled;
576
+ this.saveSources();
577
+ }
578
+
579
+ toggleLocked(source: ConfigurableSource) {
580
+ source.locked = !source.locked;
581
+ this.saveSources();
582
+ }
583
+
584
+ selectCalendar(calendarId: string) {
585
+ this.activeCalendarId = calendarId;
586
+ this.dispatchEvent(
587
+ new CustomEvent("active-calendar-changed", {
588
+ detail: { calendarId },
589
+ bubbles: true,
590
+ }),
591
+ );
592
+ }
593
+
594
+ saveForm() {
595
+ if (!this.formData.name?.trim()) {
596
+ alert("Please enter a calendar name");
597
+ return;
598
+ }
599
+
600
+ if (!this.formData.type) {
601
+ alert("Please select a calendar type");
602
+ return;
603
+ }
604
+
605
+ let source: ConfigurableSource;
606
+
607
+ if (this.formData.type === "caldav") {
608
+ if (
609
+ !this.formData.credentials?.serverUrl ||
610
+ !this.formData.credentials?.username ||
611
+ !this.formData.credentials?.password
612
+ ) {
613
+ return;
614
+ }
615
+
616
+ source = {
617
+ id: this.editingId || crypto.randomUUID(),
618
+ name: this.formData.name,
619
+ type: "caldav",
620
+ credentials: {
621
+ serverUrl: this.formData.credentials.serverUrl,
622
+ username: this.formData.credentials.username,
623
+ password: this.formData.credentials.password,
624
+ },
625
+ color: this.formData.color || "#FF6E68",
626
+ enabled: this.formData.enabled ?? true,
627
+ locked: this.formData.locked ?? false,
628
+ } as CalDAVSourceConfig;
629
+ } else if (this.formData.type === "ical") {
630
+ if (!this.formData.credentials?.url) {
631
+ return;
632
+ }
633
+
634
+ source = {
635
+ id: this.editingId || crypto.randomUUID(),
636
+ name: this.formData.name,
637
+ type: "ical",
638
+ credentials: {
639
+ url: this.formData.credentials.url,
640
+ },
641
+ color: this.formData.color || "#FF6E68",
642
+ enabled: this.formData.enabled ?? true,
643
+ locked: this.formData.locked ?? false,
644
+ } as ICalSource;
645
+ } else if (this.formData.type === "timeseries-json") {
646
+ if (!this.formData.credentials?.url) {
647
+ return;
648
+ }
649
+
650
+ source = {
651
+ id: this.editingId || crypto.randomUUID(),
652
+ name: this.formData.name,
653
+ type: "timeseries-json",
654
+ credentials: {
655
+ url: this.formData.credentials.url,
656
+ },
657
+ color: this.formData.color || "#06B6D4",
658
+ enabled: this.formData.enabled ?? true,
659
+ locked: this.formData.locked ?? false,
660
+ } as TimeseriesJsonSource;
661
+ } else if (this.formData.type === "google") {
662
+ if (!this.formData.credentials?.accessToken) {
663
+ alert("Please authenticate with Google before adding the calendar");
664
+ return;
665
+ }
666
+
667
+ source = {
668
+ id: this.editingId || crypto.randomUUID(),
669
+ name: this.formData.name,
670
+ type: "google",
671
+ credentials: {
672
+ accessToken: this.formData.credentials.accessToken,
673
+ refreshToken: this.formData.credentials.refreshToken,
674
+ tokenExpiry: this.formData.credentials.tokenExpiry,
675
+ calendarId: this.formData.credentials.calendarId || "primary",
676
+ },
677
+ color: this.formData.color || "#4285F4",
678
+ enabled: this.formData.enabled ?? true,
679
+ locked: this.formData.locked ?? false,
680
+ } as GoogleSource;
681
+ } else if (this.formData.type === "inhouse") {
682
+ if (
683
+ !this.formData.credentials?.sessionCookie ||
684
+ !this.formData.credentials?.employeeId
685
+ ) {
686
+ alert("Please enter session cookie and employee ID");
687
+ return;
688
+ }
689
+
690
+ source = {
691
+ id: this.editingId || crypto.randomUUID(),
692
+ name: this.formData.name,
693
+ type: "inhouse",
694
+ credentials: {
695
+ sessionCookie: this.formData.credentials.sessionCookie,
696
+ employeeId: this.formData.credentials.employeeId,
697
+ unitId: this.formData.credentials.unitId,
698
+ startHour: this.formData.credentials.startHour,
699
+ },
700
+ color: this.formData.color || "#FF6E68",
701
+ enabled: this.formData.enabled ?? true,
702
+ locked: this.formData.locked ?? false,
703
+ } as InhouseSource;
704
+ } else {
705
+ return;
706
+ }
707
+
708
+ if (this.editingId) {
709
+ const index = this.sources.findIndex((s) => s.id === this.editingId);
710
+ if (index >= 0) {
711
+ this.sources[index] = source;
712
+ }
713
+ } else {
714
+ this.sources.push(source);
715
+ }
716
+
717
+ this.isAdding = false;
718
+ this.editingId = null;
719
+ this.formData = {};
720
+ this.googleAuthError = null;
721
+ this.saveSources();
722
+ this.requestUpdate();
723
+ }
724
+
725
+ cancelForm() {
726
+ this.isAdding = false;
727
+ this.editingId = null;
728
+ this.formData = {};
729
+ this.googleAuthError = null;
730
+ this.isGoogleAuthenticating = false;
731
+ this.requestUpdate();
732
+ }
733
+
734
+ updateForm(field: string, value: string | boolean | number) {
735
+ if (
736
+ field === "serverUrl" ||
737
+ field === "username" ||
738
+ field === "password" ||
739
+ field === "url" ||
740
+ field === "accessToken" ||
741
+ field === "refreshToken" ||
742
+ field === "tokenExpiry" ||
743
+ field === "calendarId" ||
744
+ field === "sessionCookie" ||
745
+ field === "employeeId" ||
746
+ field === "unitId" ||
747
+ field === "startHour"
748
+ ) {
749
+ this.formData = {
750
+ ...this.formData,
751
+ credentials: {
752
+ ...this.formData.credentials,
753
+ [field]: value,
754
+ } as ConfigurableSource["credentials"],
755
+ };
756
+ } else {
757
+ this.formData = { ...this.formData, [field]: value };
758
+ }
759
+ this.requestUpdate();
760
+ }
761
+
762
+ async signInWithGoogle() {
763
+ this.isGoogleAuthenticating = true;
764
+ this.googleAuthError = null;
765
+ this.requestUpdate();
766
+
767
+ try {
768
+ // Load Google OAuth credentials from public directory
769
+ const credentialsResponse = await fetch("/credentials_google.json");
770
+ if (!credentialsResponse.ok) {
771
+ throw new Error(
772
+ "Google OAuth credentials file not found. Please add credentials_google.json to the public directory.",
773
+ );
774
+ }
775
+
776
+ const credentials = await credentialsResponse.json();
777
+ const clientId =
778
+ credentials.installed?.client_id || credentials.web?.client_id;
779
+ const clientSecret =
780
+ credentials.installed?.client_secret || credentials.web?.client_secret;
781
+
782
+ if (!clientId || !clientSecret) {
783
+ throw new Error(
784
+ "Invalid credentials file format. Expected client_id and client_secret.",
785
+ );
786
+ }
787
+
788
+ const tokens = await authenticateWithGoogle(clientId, clientSecret);
789
+
790
+ // Fetch the list of calendars from Google
791
+ const calendarListResponse = await fetch(
792
+ "https://www.googleapis.com/calendar/v3/users/me/calendarList",
793
+ {
794
+ headers: {
795
+ Authorization: `Bearer ${tokens.accessToken}`,
796
+ },
797
+ },
798
+ );
799
+
800
+ if (!calendarListResponse.ok) {
801
+ throw new Error("Failed to fetch calendar list from Google");
802
+ }
803
+
804
+ const calendarList = await calendarListResponse.json();
805
+
806
+ // Create a source for each calendar
807
+ for (const calendar of calendarList.items || []) {
808
+ const newSource: GoogleSource = {
809
+ id: crypto.randomUUID(),
810
+ name: calendar.summary || "Untitled Calendar",
811
+ type: "google",
812
+ credentials: {
813
+ accessToken: tokens.accessToken,
814
+ refreshToken: tokens.refreshToken,
815
+ tokenExpiry: tokens.expiry,
816
+ calendarId: calendar.id,
817
+ },
818
+ color: calendar.backgroundColor || "#4285F4",
819
+ enabled: true,
820
+ };
821
+
822
+ this.sources.push(newSource);
823
+ }
824
+
825
+ this.saveSources();
826
+ this.isAdding = false;
827
+ this.formData = {};
828
+ this.isGoogleAuthenticating = false;
829
+ this.requestUpdate();
830
+ } catch (error) {
831
+ this.isGoogleAuthenticating = false;
832
+ this.googleAuthError =
833
+ error instanceof Error ? error.message : "Authentication failed";
834
+ this.requestUpdate();
835
+ }
836
+ }
837
+
838
+ close() {
839
+ this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
840
+ }
841
+
842
+ render() {
843
+ const colors = [
844
+ "#FF6E68",
845
+ "#FFB800",
846
+ "#83D754",
847
+ "#0095FD",
848
+ "#9FC6E7",
849
+ "#888082",
850
+ "#E91E63",
851
+ "#9C27B0",
852
+ "#673AB7",
853
+ "#3F51B5",
854
+ "#2196F3",
855
+ "#00BCD4",
856
+ "#009688",
857
+ "#4CAF50",
858
+ "#8BC34A",
859
+ "#CDDC39",
860
+ "#FFEB3B",
861
+ "#FFC107",
862
+ "#FF9800",
863
+ "#FF5722",
864
+ "#795548",
865
+ ];
866
+
867
+ return html`
839
868
  <div class="container">
840
869
  <div class="header">
841
870
  <span class="title">Sources</span>
842
- <button class="collapse-btn" @click=${this.toggleCollapsed} title="${this.collapsed ? "Expand" : "Collapse"}">
871
+ <button class="collapse-btn" @click=${this.toggleCollapsed} title="${
872
+ this.collapsed ? "Expand" : "Collapse"
873
+ }">
843
874
  ${this.collapsed ? "▶" : "◀"}
844
875
  </button>
845
876
  </div>
846
877
 
847
- ${this.collapsed
848
- ? this.renderSourcesList()
849
- : (this.isAdding || this.editingId
850
- ? this.renderForm(colors)
851
- : this.renderSourcesList())}
878
+ ${
879
+ this.collapsed
880
+ ? this.renderSourcesList()
881
+ : this.isAdding || this.editingId
882
+ ? this.renderForm(colors)
883
+ : this.renderSourcesList()
884
+ }
852
885
  </div>
853
886
  `;
854
- }
887
+ }
855
888
 
856
- renderSourcesList() {
857
- if (this.sources.length === 0) {
858
- return html`
889
+ renderSourcesList() {
890
+ if (this.sources.length === 0) {
891
+ return html`
859
892
  <div class="empty-state">
860
893
  No calendar sources configured
861
894
  </div>
@@ -863,18 +896,24 @@ export class CalDAVConfigElement extends LitElement {
863
896
  + Add Source
864
897
  </button>
865
898
  `;
866
- }
899
+ }
867
900
 
868
- return html`
901
+ return html`
869
902
  <div class="sources-list">
870
- ${this.sources.map(
871
- (source) => {
872
- const sourceCalendars = this.calendars.filter(c => c.sourceId === source.id);
873
- return html`
874
- <div class="source-item" @click=${() => this.collapsed ? this.toggleEnabled(source) : this.editSource(source)}>
903
+ ${this.sources.map((source) => {
904
+ const sourceCalendars = this.calendars.filter(
905
+ (c) => c.sourceId === source.id,
906
+ );
907
+ return html`
908
+ <div class="source-item" @click=${() =>
909
+ this.collapsed
910
+ ? this.toggleEnabled(source)
911
+ : this.editSource(source)}>
875
912
  <div
876
913
  class="source-color ${source.enabled ? "" : "disabled"}"
877
- style="background: ${source.color}; --source-color: ${source.color}"
914
+ style="background: ${source.color}; --source-color: ${
915
+ source.color
916
+ }"
878
917
  ></div>
879
918
  <span class="source-name">${source.name}</span>
880
919
  <input
@@ -886,34 +925,43 @@ export class CalDAVConfigElement extends LitElement {
886
925
  title="${source.enabled ? "Disable" : "Enable"} sync"
887
926
  />
888
927
  </div>
889
- ${source.enabled && sourceCalendars.length > 0 ? html`
928
+ ${
929
+ source.enabled && sourceCalendars.length > 0
930
+ ? html`
890
931
  <div class="calendar-list">
891
- ${sourceCalendars.map(cal => html`
932
+ ${sourceCalendars.map(
933
+ (cal) => html`
892
934
  <div
893
- class="calendar-item ${this.activeCalendarId === cal.id ? "active" : ""}"
935
+ class="calendar-item ${
936
+ this.activeCalendarId === cal.id ? "active" : ""
937
+ }"
894
938
  @click=${() => this.selectCalendar(cal.id)}
895
939
  >
896
- <div class="calendar-color" style="background: ${cal.color}"></div>
940
+ <div class="calendar-color" style="background: ${
941
+ cal.color
942
+ }"></div>
897
943
  <span class="calendar-name">${cal.name}</span>
898
944
  </div>
899
- `)}
945
+ `,
946
+ )}
900
947
  </div>
901
- ` : null}
948
+ `
949
+ : null
950
+ }
902
951
  `;
903
- }
904
- )}
952
+ })}
905
953
  </div>
906
954
  <button class="add-btn" @click=${this.addSource}>
907
955
  + Add Source
908
956
  </button>
909
957
  `;
910
- }
958
+ }
911
959
 
912
- renderForm(colors: string[]) {
913
- const isEditing = this.editingId !== null;
914
- const sourceType = this.formData.type || "caldav";
960
+ renderForm(colors: string[]) {
961
+ const isEditing = this.editingId !== null;
962
+ const sourceType = this.formData.type || "caldav";
915
963
 
916
- return html`
964
+ return html`
917
965
  <div class="form">
918
966
  <div class="form-group">
919
967
  <label class="form-label">Type</label>
@@ -921,17 +969,17 @@ export class CalDAVConfigElement extends LitElement {
921
969
  class="form-input"
922
970
  .value=${sourceType}
923
971
  @change=${(e: Event) => {
924
- const type = (e.target as HTMLSelectElement).value;
925
- this.updateForm("type", type);
926
- // Set default color based on type
927
- if (type === "google" && !this.formData.color) {
928
- this.updateForm("color", "#4285F4");
929
- } else if (type === "caldav" && !this.formData.color) {
930
- this.updateForm("color", "#FF6E68");
931
- } else if (type === "timeseries-json" && !this.formData.color) {
932
- this.updateForm("color", "#06B6D4");
933
- }
934
- }}
972
+ const type = (e.target as HTMLSelectElement).value;
973
+ this.updateForm("type", type);
974
+ // Set default color based on type
975
+ if (type === "google" && !this.formData.color) {
976
+ this.updateForm("color", "#4285F4");
977
+ } else if (type === "caldav" && !this.formData.color) {
978
+ this.updateForm("color", "#FF6E68");
979
+ } else if (type === "timeseries-json" && !this.formData.color) {
980
+ this.updateForm("color", "#06B6D4");
981
+ }
982
+ }}
935
983
  ?disabled=${isEditing}
936
984
  >
937
985
  <option value="caldav">CalDAV (with credentials)</option>
@@ -950,12 +998,13 @@ export class CalDAVConfigElement extends LitElement {
950
998
  placeholder="My Calendar"
951
999
  .value=${this.formData.name || ""}
952
1000
  @input=${(e: Event) =>
953
- this.updateForm("name", (e.target as HTMLInputElement).value)}
1001
+ this.updateForm("name", (e.target as HTMLInputElement).value)}
954
1002
  />
955
1003
  </div>
956
1004
 
957
- ${sourceType === "caldav"
958
- ? html`
1005
+ ${
1006
+ sourceType === "caldav"
1007
+ ? html`
959
1008
  <div class="form-group">
960
1009
  <label class="form-label">Server URL</label>
961
1010
  <input
@@ -964,7 +1013,10 @@ export class CalDAVConfigElement extends LitElement {
964
1013
  placeholder="https://mail.example.com/caldav/users/username/"
965
1014
  .value=${this.formData.credentials?.serverUrl || ""}
966
1015
  @input=${(e: Event) =>
967
- this.updateForm("serverUrl", (e.target as HTMLInputElement).value)}
1016
+ this.updateForm(
1017
+ "serverUrl",
1018
+ (e.target as HTMLInputElement).value,
1019
+ )}
968
1020
  />
969
1021
  </div>
970
1022
 
@@ -975,7 +1027,10 @@ export class CalDAVConfigElement extends LitElement {
975
1027
  type="text"
976
1028
  .value=${this.formData.credentials?.username || ""}
977
1029
  @input=${(e: Event) =>
978
- this.updateForm("username", (e.target as HTMLInputElement).value)}
1030
+ this.updateForm(
1031
+ "username",
1032
+ (e.target as HTMLInputElement).value,
1033
+ )}
979
1034
  />
980
1035
  </div>
981
1036
 
@@ -986,12 +1041,15 @@ export class CalDAVConfigElement extends LitElement {
986
1041
  type="password"
987
1042
  .value=${this.formData.credentials?.password || ""}
988
1043
  @input=${(e: Event) =>
989
- this.updateForm("password", (e.target as HTMLInputElement).value)}
1044
+ this.updateForm(
1045
+ "password",
1046
+ (e.target as HTMLInputElement).value,
1047
+ )}
990
1048
  />
991
1049
  </div>
992
1050
  `
993
- : sourceType === "ical"
994
- ? html`
1051
+ : sourceType === "ical"
1052
+ ? html`
995
1053
  <div class="form-group">
996
1054
  <label class="form-label">iCal URL</label>
997
1055
  <input
@@ -1000,12 +1058,15 @@ export class CalDAVConfigElement extends LitElement {
1000
1058
  placeholder="https://example.com/calendar.ics"
1001
1059
  .value=${this.formData.credentials?.url || ""}
1002
1060
  @input=${(e: Event) =>
1003
- this.updateForm("url", (e.target as HTMLInputElement).value)}
1061
+ this.updateForm(
1062
+ "url",
1063
+ (e.target as HTMLInputElement).value,
1064
+ )}
1004
1065
  />
1005
1066
  </div>
1006
1067
  `
1007
- : sourceType === "timeseries-json"
1008
- ? html`
1068
+ : sourceType === "timeseries-json"
1069
+ ? html`
1009
1070
  <div class="form-group">
1010
1071
  <label class="form-label">Timeseries JSON URL</label>
1011
1072
  <input
@@ -1014,31 +1075,36 @@ export class CalDAVConfigElement extends LitElement {
1014
1075
  placeholder="https://example.com/data.json"
1015
1076
  .value=${this.formData.credentials?.url || ""}
1016
1077
  @input=${(e: Event) =>
1017
- this.updateForm("url", (e.target as HTMLInputElement).value)}
1078
+ this.updateForm(
1079
+ "url",
1080
+ (e.target as HTMLInputElement).value,
1081
+ )}
1018
1082
  />
1019
1083
  <small style="color: var(--text-muted, rgba(255, 255, 255, 0.5)); font-size: 11px;">
1020
1084
  Expected format: JSON array of objects with a <code>timestamp</code> field. Rendered as a heatmap backdrop.
1021
1085
  </small>
1022
1086
  </div>
1023
1087
  `
1024
- : sourceType === "google"
1025
- ? html`
1088
+ : sourceType === "google"
1089
+ ? html`
1026
1090
  <div class="google-auth-section">
1027
- ${this.formData.credentials?.accessToken
1028
- ? html`
1091
+ ${
1092
+ this.formData.credentials?.accessToken
1093
+ ? html`
1029
1094
  <div class="auth-status authenticated">
1030
1095
  ✓ Authenticated with Google
1031
1096
  </div>
1032
1097
  `
1033
- : html`
1098
+ : html`
1034
1099
  <button
1035
1100
  class="google-login-btn"
1036
1101
  @click=${this.signInWithGoogle}
1037
1102
  ?disabled=${this.isGoogleAuthenticating}
1038
1103
  >
1039
- ${this.isGoogleAuthenticating
1040
- ? html`<span>Signing in...</span>`
1041
- : html`
1104
+ ${
1105
+ this.isGoogleAuthenticating
1106
+ ? html`<span>Signing in...</span>`
1107
+ : html`
1042
1108
  <svg class="google-logo" viewBox="0 0 24 24">
1043
1109
  <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
1044
1110
  <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
@@ -1046,12 +1112,16 @@ export class CalDAVConfigElement extends LitElement {
1046
1112
  <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
1047
1113
  </svg>
1048
1114
  <span>Sign in with Google</span>
1049
- `}
1115
+ `
1116
+ }
1050
1117
  </button>
1051
- `}
1052
- ${this.googleAuthError
1053
- ? html`<div class="auth-status error">${this.googleAuthError}</div>`
1054
- : null}
1118
+ `
1119
+ }
1120
+ ${
1121
+ this.googleAuthError
1122
+ ? html`<div class="auth-status error">${this.googleAuthError}</div>`
1123
+ : null
1124
+ }
1055
1125
  </div>
1056
1126
 
1057
1127
  <div class="form-group">
@@ -1062,12 +1132,15 @@ export class CalDAVConfigElement extends LitElement {
1062
1132
  placeholder="primary"
1063
1133
  .value=${this.formData.credentials?.calendarId || ""}
1064
1134
  @input=${(e: Event) =>
1065
- this.updateForm("calendarId", (e.target as HTMLInputElement).value)}
1135
+ this.updateForm(
1136
+ "calendarId",
1137
+ (e.target as HTMLInputElement).value,
1138
+ )}
1066
1139
  />
1067
1140
  </div>
1068
1141
  `
1069
- : sourceType === "inhouse"
1070
- ? html`
1142
+ : sourceType === "inhouse"
1143
+ ? html`
1071
1144
  <div class="form-group">
1072
1145
  <label class="form-label">Session Cookie</label>
1073
1146
  <input
@@ -1076,7 +1149,10 @@ export class CalDAVConfigElement extends LitElement {
1076
1149
  placeholder="sessionid=abc123; csrftoken=xyz789"
1077
1150
  .value=${this.formData.credentials?.sessionCookie || ""}
1078
1151
  @input=${(e: Event) =>
1079
- this.updateForm("sessionCookie", (e.target as HTMLInputElement).value)}
1152
+ this.updateForm(
1153
+ "sessionCookie",
1154
+ (e.target as HTMLInputElement).value,
1155
+ )}
1080
1156
  />
1081
1157
  <small style="color: var(--text-muted, rgba(255, 255, 255, 0.5)); font-size: 11px;">
1082
1158
  Copy from browser DevTools after logging in
@@ -1091,7 +1167,10 @@ export class CalDAVConfigElement extends LitElement {
1091
1167
  placeholder="589"
1092
1168
  .value=${this.formData.credentials?.employeeId || ""}
1093
1169
  @input=${(e: Event) =>
1094
- this.updateForm("employeeId", (e.target as HTMLInputElement).value)}
1170
+ this.updateForm(
1171
+ "employeeId",
1172
+ (e.target as HTMLInputElement).value,
1173
+ )}
1095
1174
  />
1096
1175
  </div>
1097
1176
 
@@ -1103,7 +1182,10 @@ export class CalDAVConfigElement extends LitElement {
1103
1182
  placeholder="3"
1104
1183
  .value=${this.formData.credentials?.unitId || ""}
1105
1184
  @input=${(e: Event) =>
1106
- this.updateForm("unitId", (e.target as HTMLInputElement).value)}
1185
+ this.updateForm(
1186
+ "unitId",
1187
+ (e.target as HTMLInputElement).value,
1188
+ )}
1107
1189
  />
1108
1190
  </div>
1109
1191
 
@@ -1111,40 +1193,55 @@ export class CalDAVConfigElement extends LitElement {
1111
1193
  <label class="form-label">Bookings Start At</label>
1112
1194
  <select
1113
1195
  class="form-input"
1114
- .value=${String(this.formData.credentials?.startHour ?? 9)}
1196
+ .value=${String(
1197
+ this.formData.credentials?.startHour ?? 9,
1198
+ )}
1115
1199
  @change=${(e: Event) =>
1116
- this.updateForm("startHour", Number((e.target as HTMLSelectElement).value))}
1200
+ this.updateForm(
1201
+ "startHour",
1202
+ Number((e.target as HTMLSelectElement).value),
1203
+ )}
1117
1204
  >
1118
- <option value="9" ?selected=${(this.formData.credentials?.startHour ?? 9) === 9}>9:00 AM</option>
1119
- <option value="10" ?selected=${this.formData.credentials?.startHour === 10}>10:00 AM</option>
1205
+ <option value="9" ?selected=${
1206
+ (this.formData.credentials?.startHour ?? 9) === 9
1207
+ }>9:00 AM</option>
1208
+ <option value="10" ?selected=${
1209
+ this.formData.credentials?.startHour === 10
1210
+ }>10:00 AM</option>
1120
1211
  </select>
1121
1212
  </div>
1122
1213
  `
1123
- : null}
1214
+ : null
1215
+ }
1124
1216
 
1125
1217
  <div class="form-group">
1126
1218
  <label class="form-label">Color</label>
1127
1219
  <div class="color-picker">
1128
1220
  ${colors.map(
1129
- (color) => html`
1221
+ (color) => html`
1130
1222
  <div
1131
- class="color-option ${this.formData.color === color
1132
- ? "selected"
1133
- : ""}"
1223
+ class="color-option ${
1224
+ this.formData.color === color ? "selected" : ""
1225
+ }"
1134
1226
  style="background: ${color}"
1135
1227
  @click=${() => this.updateForm("color", color)}
1136
1228
  ></div>
1137
- `
1138
- )}
1229
+ `,
1230
+ )}
1139
1231
  </div>
1140
1232
  </div>
1141
1233
 
1142
1234
  <div class="form-actions">
1143
- ${isEditing ? html`
1144
- <button class="btn btn-danger" @click=${() => this.deleteSource(this.editingId!)}>
1235
+ ${
1236
+ isEditing
1237
+ ? html`
1238
+ <button class="btn btn-danger" @click=${() =>
1239
+ this.deleteSource(this.editingId!)}>
1145
1240
  Delete
1146
1241
  </button>
1147
- ` : null}
1242
+ `
1243
+ : null
1244
+ }
1148
1245
  <button class="btn btn-secondary" @click=${this.cancelForm}>
1149
1246
  Cancel
1150
1247
  </button>
@@ -1154,11 +1251,11 @@ export class CalDAVConfigElement extends LitElement {
1154
1251
  </div>
1155
1252
  </div>
1156
1253
  `;
1157
- }
1254
+ }
1158
1255
  }
1159
1256
 
1160
1257
  try {
1161
- customElements.define("caldav-config", CalDAVConfigElement);
1258
+ customElements.define("caldav-config", CalDAVConfigElement);
1162
1259
  } catch (error) {
1163
- console.error("Failed to register custom element:", error);
1260
+ console.error("Failed to register custom element:", error);
1164
1261
  }