This commit is contained in:
Chris Chen
2026-05-28 22:29:13 -07:00
parent a2d394029a
commit 0639d1fe83
23 changed files with 896 additions and 158 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
+4 -2
View File
@@ -48,7 +48,8 @@
"src/assets"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/tailwind.css"
]
},
"configurations": {
@@ -106,7 +107,8 @@
"src/assets"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/tailwind.css"
]
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Escrow Portal Access - ROLCC AC</title>
<title>Church Portal Access - ROLCC AC</title>
<style>
/* Email-safe CSS - compatible with most email clients */
body {
@@ -233,7 +233,7 @@
<tr>
<td class="email-header"
style="background-color: #1e3a8a; padding: 30px 20px; text-align: center; color: white;">
<img src="assets/rbj-logo.svg" alt="RBJ Logo" class="logo-image"
<img src="assets/images/ROLCC-Logo-Color.png" alt="ROLCC Logo" class="logo-image"
style="height: 50px; width: auto; margin-bottom: 15px;">
<div class="logo-text">
<h1 style="font-size: 28px; font-weight: bold; margin: 0 0 5px 0;">ROLCC AC</h1>
+621 -8
View File
@@ -63,6 +63,7 @@
"@angular/cli": "^20.1.6",
"@angular/compiler-cli": "^20.1.0",
"@angular/localize": "^20.2.1",
"@tailwindcss/postcss": "^4.3.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.8.0",
"karma": "~6.4.0",
@@ -70,6 +71,8 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.15",
"tailwindcss": "^4.3.0",
"typescript": "~5.8.2"
}
},
@@ -282,6 +285,19 @@
"node": ">= 14.0.0"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -1910,6 +1926,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -4830,6 +4857,287 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
"integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.21.0",
"jiti": "^2.6.1",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.0"
}
},
"node_modules/@tailwindcss/node/node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
"integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-x64": "4.3.0",
"@tailwindcss/oxide-freebsd-x64": "4.3.0",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-x64-musl": "4.3.0",
"@tailwindcss/oxide-wasm32-wasi": "4.3.0",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
"integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
"integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
"integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
"integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
"integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
"integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
"integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
"integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
"integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
"integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@emnapi/wasi-threads": "^1.2.1",
"@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
"integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
"integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz",
"integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.3.0",
"@tailwindcss/oxide": "4.3.0",
"postcss": "^8.5.10",
"tailwindcss": "4.3.0"
}
},
"node_modules/@tufjs/canonical-json": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
@@ -5930,7 +6238,6 @@
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -6178,6 +6485,20 @@
"node": ">= 0.6"
}
},
"node_modules/enhanced-resolve": {
"version": "5.22.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz",
"integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/ent": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz",
@@ -7337,6 +7658,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -7924,6 +8255,267 @@
"node": ">=10"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/listr2": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
@@ -8597,9 +9189,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -9376,9 +9968,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -9396,7 +9988,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -10576,6 +11168,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+4 -1
View File
@@ -77,6 +77,7 @@
"@angular/cli": "^20.1.6",
"@angular/compiler-cli": "^20.1.0",
"@angular/localize": "^20.2.1",
"@tailwindcss/postcss": "^4.3.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.8.0",
"karma": "~6.4.0",
@@ -84,6 +85,8 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.15",
"tailwindcss": "^4.3.0",
"typescript": "~5.8.2"
}
}
}
@@ -1,10 +1,10 @@
<kendo-dialog title="Quick add member" (close)="cancelled.emit()" [width]="420">
<div style="display:flex;flex-direction:column;gap:0.75rem;">
<label>First name (EN) *<kendo-textbox [(ngModel)]="firstName_en"></kendo-textbox></label>
<label>Last name (EN) *<kendo-textbox [(ngModel)]="lastName_en"></kendo-textbox></label>
<label>名 (中)<kendo-textbox [(ngModel)]="firstName_zh"></kendo-textbox></label>
<label>姓 (中)<kendo-textbox [(ngModel)]="lastName_zh"></kendo-textbox></label>
<label>Cell phone<kendo-textbox [(ngModel)]="phoneCell"></kendo-textbox></label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1">First name (EN) *<kendo-textbox [(ngModel)]="firstName_en"></kendo-textbox></label>
<label class="flex flex-col gap-1">Last name (EN) *<kendo-textbox [(ngModel)]="lastName_en"></kendo-textbox></label>
<label class="flex flex-col gap-1">名 (中)<kendo-textbox [(ngModel)]="firstName_zh"></kendo-textbox></label>
<label class="flex flex-col gap-1">姓 (中)<kendo-textbox [(ngModel)]="lastName_zh"></kendo-textbox></label>
<label class="flex flex-col gap-1 md:col-span-2">Cell phone<kendo-textbox [(ngModel)]="phoneCell"></kendo-textbox></label>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="cancelled.emit()">Cancel</button>
@@ -25,28 +25,28 @@
</kendo-grid>
<kendo-dialog *ngIf="showDialog" [title]="editing ? 'Edit Giving Type' : 'Add Giving Type'" (close)="showDialog=false" [width]="480">
<div class="form-grid">
<label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1">
Name (EN) *
<kendo-textbox [(ngModel)]="form.name_en"></kendo-textbox>
</label>
<label>
<label class="flex flex-col gap-1">
名稱 (中)
<kendo-textbox [(ngModel)]="form.name_zh"></kendo-textbox>
</label>
<label>
<label class="flex flex-col gap-1">
Description (EN)
<kendo-textbox [(ngModel)]="form.description_en"></kendo-textbox>
</label>
<label>
<label class="flex flex-col gap-1">
說明 (中)
<kendo-textbox [(ngModel)]="form.description_zh"></kendo-textbox>
</label>
<label>
<label class="flex flex-col gap-1">
Sort order
<kendo-numerictextbox [(ngModel)]="form.sortOrder" [format]="'n0'" [decimals]="0" [min]="0"></kendo-numerictextbox>
</label>
<label *ngIf="editing">
<label *ngIf="editing" class="flex items-center gap-2 md:col-span-2">
<input type="checkbox" [(ngModel)]="form.isActive" /> Active
</label>
</div>
@@ -17,15 +17,3 @@
gap: 0.25rem;
cursor: pointer;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-grid label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
@@ -30,12 +30,12 @@
</kendo-grid>
<kendo-dialog *ngIf="showDialog" title="Add Giving" (close)="showDialog=false" [width]="520">
<div class="form-grid">
<label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex items-center gap-2 md:col-span-2">
<input type="checkbox" [ngModel]="form.isAnonymous" (ngModelChange)="toggleAnonymous()" /> Anonymous
</label>
<label *ngIf="!form.isAnonymous">Giver
<label *ngIf="!form.isAnonymous" class="flex flex-col gap-1 md:col-span-2">Giver
<kendo-dropdownlist [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true"
(filterChange)="onMemberFilter($event)"
@@ -44,34 +44,34 @@
placeholder="Search member by name"></kendo-dropdownlist>
</label>
<label>Type
<label class="flex flex-col gap-1">Type
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id"
[valuePrimitive]="true" [(ngModel)]="form.givingCategoryId"></kendo-dropdownlist>
</label>
<label>Payment method
<label class="flex flex-col gap-1">Payment method
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="form.paymentMethod"></kendo-dropdownlist>
</label>
<label *ngIf="form.paymentMethod === 'Check'">Check #
<label *ngIf="form.paymentMethod === 'Check'" class="flex flex-col gap-1">Check #
<kendo-textbox [(ngModel)]="form.checkNumber"></kendo-textbox>
</label>
<label *ngIf="form.paymentMethod === 'Zelle'">Zelle ref
<label *ngIf="form.paymentMethod === 'Zelle'" class="flex flex-col gap-1">Zelle ref
<kendo-textbox [(ngModel)]="form.zelleReferenceCode"></kendo-textbox>
</label>
<label *ngIf="form.paymentMethod === 'PayPal'">PayPal txn
<label *ngIf="form.paymentMethod === 'PayPal'" class="flex flex-col gap-1">PayPal txn
<kendo-textbox [(ngModel)]="form.payPalTransactionId"></kendo-textbox>
</label>
<label>Amount
<label class="flex flex-col gap-1">Amount
<kendo-numerictextbox [(ngModel)]="form.amount" [min]="0" [format]="'c2'"></kendo-numerictextbox>
</label>
<label>Date
<label class="flex flex-col gap-1">Date
<kendo-datepicker [(ngModel)]="givingDateValue"></kendo-datepicker>
</label>
<label>Notes
<label class="flex flex-col gap-1 md:col-span-2">Notes
<kendo-textbox [(ngModel)]="form.notes"></kendo-textbox>
</label>
</div>
@@ -1,4 +1,2 @@
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.form-grid { display: flex; flex-direction: column; gap: 0.75rem; }
.form-grid label { display: flex; flex-direction: column; gap: 0.25rem; }
@@ -120,9 +120,7 @@ export class GivingsPageComponent implements OnInit {
save(): void {
// Sync the datepicker Date back to the ISO string field.
this.form.givingDate = this.givingDateValue
? this.givingDateValue.toISOString().slice(0, 10)
: new Date().toISOString().slice(0, 10);
this.form.givingDate = this.toIso(this.givingDateValue ?? new Date());
if (this.editingId) {
this.api.update(this.editingId, this.form).subscribe(() => { this.showDialog = false; this.load(); });
@@ -149,9 +147,18 @@ export class GivingsPageComponent implements OnInit {
checkNumber: null,
zelleReferenceCode: null,
payPalTransactionId: null,
givingDate: new Date().toISOString().slice(0, 10),
givingDate: this.toIso(new Date()),
isAnonymous: false,
notes: null,
};
}
// Format using LOCAL date components — NOT toISOString(), which converts to UTC and
// rolls the date forward a day for behind-UTC users when the Date carries an evening time.
private toIso(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
}
@@ -1,92 +1,142 @@
<div class="page">
<header class="page-header">
<h2>Sunday Offering Entry / 主日奉獻錄入</h2>
<label>Date
<kendo-datepicker [(ngModel)]="sessionDate" (valueChange)="checkDate()" [disabled]="editingSessionId != null"></kendo-datepicker>
</label>
</header>
<!-- Card A — Session header -->
<section class="card">
<header class="page-header">
<h2 style="margin:0">Sunday Offering Entry / 主日奉獻錄入</h2>
<label class="flex flex-col gap-1">Date
<kendo-datepicker [(ngModel)]="sessionDate" (valueChange)="checkDate()"
[disabled]="editingSessionId != null"></kendo-datepicker>
</label>
</header>
<div *ngIf="editingSessionId != null" class="edit-banner">
Editing submitted session — make changes and click "Update Session".
<button kendoButton fillMode="flat" (click)="cancelEdit()">Cancel edit</button>
</div>
<div *ngIf="editingSessionId != null" class="edit-banner">
<span class="badge badge--draft">Editing</span>
Editing session — make changes and click "Update Session".
<button kendoButton fillMode="flat" (click)="cancelEdit()">Cancel edit</button>
</div>
<div *ngIf="dateConflict && editingSessionId == null" class="warn">
An offering session for this date already exists. Pick another date, or reopen the existing session to edit.
</div>
<section class="entry-row">
<label *ngIf="!entry.isAnonymous">Giver
<kendo-dropdownlist [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true"
(filterChange)="onMemberFilter($event)" [(ngModel)]="selectedMemberId"
(valueChange)="onMemberSelected($event)" placeholder="Search by name"></kendo-dropdownlist>
</label>
<span *ngIf="entry.isAnonymous" class="anon-chip">Anonymous</span>
<label>Type
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id"
[valuePrimitive]="true" [(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>
</label>
<label>Method
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
</label>
<label *ngIf="entry.paymentMethod === 'Check'">Check #<kendo-textbox [(ngModel)]="entry.checkNumber"></kendo-textbox></label>
<label>Amount
<kendo-numerictextbox [(ngModel)]="entry.amount" [min]="0" [format]="'c2'" (keydown.enter)="addLine()"></kendo-numerictextbox>
</label>
<label>Notes<kendo-textbox [(ngModel)]="entry.notes" (keydown.enter)="addLine()"></kendo-textbox></label>
<div class="entry-actions">
<button kendoButton (click)="markAnonymous()">Anonymous</button>
<button kendoButton (click)="showQuickAdd = true">+ Quick add member</button>
<button kendoButton themeColor="primary" (click)="addLine()">+ Add (Enter)</button>
<div *ngIf="dateConflict && editingSessionId == null" class="warn">
An offering session for this date already exists. Pick another date, or reopen the existing session to edit.
</div>
</section>
<kendo-grid [data]="buffer">
<kendo-grid-column title="Giver">
<ng-template kendoGridCellTemplate let-l>{{ l.isAnonymous ? '(Anonymous)' : l.memberName }}</ng-template>
</kendo-grid-column>
<kendo-grid-column field="categoryName" title="Type"></kendo-grid-column>
<kendo-grid-column field="paymentMethod" title="Method" [width]="90"></kendo-grid-column>
<kendo-grid-column field="checkNumber" title="Check #" [width]="90"></kendo-grid-column>
<kendo-grid-column field="amount" title="Amount" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="120">
<ng-template kendoGridCellTemplate let-l let-i="rowIndex">
<button kendoButton fillMode="flat" (click)="editLine(i)">Edit</button>
<button kendoButton fillMode="flat" (click)="removeLine(i)">×</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
<!-- Card B — Add giving -->
<section class="card">
<h3 class="section-title">Add Giving / 錄入奉獻</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1 md:col-span-2">Giver
<kendo-dropdownlist *ngIf="!entry.isAnonymous" [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true" (filterChange)="onMemberFilter($event)"
[(ngModel)]="selectedMemberId" (valueChange)="onMemberSelected($event)"
placeholder="Search by name"></kendo-dropdownlist>
<span *ngIf="entry.isAnonymous" class="flex items-center gap-2">
<span class="anon-chip">Anonymous</span>
<button kendoButton fillMode="flat" size="small" (click)="clearAnonymous()">Clear</button>
</span>
</label>
<section class="reconcile">
<div>Lines: {{ buffer.length }} | System total: {{ systemTotal | currency }}</div>
<label>Cash counted<kendo-numerictextbox [(ngModel)]="cashTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox></label>
<label>Check counted<kendo-numerictextbox [(ngModel)]="checkTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox></label>
<div [class.ok]="difference === 0" [class.bad]="difference !== 0">Difference: {{ difference | currency }}</div>
<button kendoButton themeColor="primary"
[disabled]="buffer.length === 0 || (editingSessionId == null && dateConflict) || submitting"
(click)="submit()">{{ editingSessionId != null ? 'Update Session' : 'Submit' }}</button>
<label class="flex flex-col gap-1">Type
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id" [valuePrimitive]="true"
[(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>
</label>
<label class="flex flex-col gap-1">Method
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
</label>
<label *ngIf="entry.paymentMethod === 'Check'" class="flex flex-col gap-1">Check #
<kendo-textbox [(ngModel)]="entry.checkNumber"></kendo-textbox>
</label>
<label class="flex flex-col gap-1">Amount
<kendo-numerictextbox [(ngModel)]="entry.amount" [min]="0" [format]="'c2'"
(keydown.enter)="addLine()"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1 md:col-span-2">Notes
<kendo-textbox [(ngModel)]="entry.notes" (keydown.enter)="addLine()"></kendo-textbox>
</label>
<div class="flex flex-wrap gap-2 md:col-span-2">
<button kendoButton (click)="markAnonymous()">Anonymous</button>
<button kendoButton (click)="showQuickAdd = true">+ Quick add member</button>
<button kendoButton themeColor="primary" (click)="addLine()">{{ editingIndex !== null ? 'Update line' : '+ Add
(Enter)' }}</button>
</div>
</div>
</section>
<section class="sessions-list">
<h3>Recent Sessions</h3>
<!-- Card C — Lines -->
<section class="card">
<h3 class="section-title">Lines / 已加入明細</h3>
<div *ngIf="buffer.length === 0" class="empty">No lines yet — add givings above.</div>
<kendo-grid *ngIf="buffer.length > 0" [data]="buffer">
<kendo-grid-column title="Giver">
<ng-template kendoGridCellTemplate let-l>{{ l.isAnonymous ? '(Anonymous)' : l.memberName }}</ng-template>
</kendo-grid-column>
<kendo-grid-column field="categoryName" title="Type"></kendo-grid-column>
<kendo-grid-column field="paymentMethod" title="Method" [width]="90"></kendo-grid-column>
<kendo-grid-column field="checkNumber" title="Check #" [width]="90"></kendo-grid-column>
<kendo-grid-column field="amount" title="Amount" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="120">
<ng-template kendoGridCellTemplate let-l let-i="rowIndex">
<button kendoButton fillMode="flat" (click)="editLine(i)">Edit</button>
<button kendoButton fillMode="flat" (click)="removeLine(i)">×</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
<div *ngIf="buffer.length > 0" class="lines-footer">Lines: {{ buffer.length }} · System total: {{ systemTotal |
currency }}</div>
</section>
<!-- Card D — Reconcile & submit -->
<section class="card">
<h3 class="section-title">Reconcile &amp; Submit / 對帳</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1">Cash counted
<kendo-numerictextbox [(ngModel)]="cashTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1">Check counted
<kendo-numerictextbox [(ngModel)]="checkTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1 md:col-span-2">Session notes
<kendo-textarea [(ngModel)]="notes" [rows]="2"></kendo-textarea>
</label>
</div>
<div class="flex flex-wrap items-center gap-4 mt-4">
<span>System total: {{ systemTotal | currency }}</span>
<span [class.ok]="difference === 0" [class.bad]="difference !== 0">Difference: {{ difference | currency }}</span>
<button kendoButton themeColor="primary"
[disabled]="buffer.length === 0 || (editingSessionId == null && dateConflict) || submitting"
(click)="submit()">{{ editingSessionId != null ? 'Update Session' : 'Submit' }}</button>
</div>
</section>
<!-- Card E — Recent sessions -->
<section class="card">
<h3 class="section-title">Recent Sessions</h3>
<kendo-grid [data]="sessions">
<kendo-grid-column field="sessionDate" title="Date" [width]="120"></kendo-grid-column>
<kendo-grid-column field="status" title="Status" [width]="110"></kendo-grid-column>
<kendo-grid-column title="Status" [width]="120">
<ng-template kendoGridCellTemplate let-s>
<span class="badge" [class.badge--draft]="s.status === 'Draft'"
[class.badge--submitted]="s.status === 'Submitted'" [class.badge--reconciled]="s.status === 'Reconciled'">{{
s.status }}</span>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="lineCount" title="Lines" [width]="80"></kendo-grid-column>
<kendo-grid-column field="systemTotal" title="System" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column field="difference" title="Diff" [width]="100" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="140">
<kendo-grid-column title="" [width]="150">
<ng-template kendoGridCellTemplate let-s>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Submitted'" (click)="reopenAndEdit(s)">Reopen &amp; Edit</button>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Submitted'" (click)="reopenAndEdit(s)">Reopen &amp;
Edit</button>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Draft'" (click)="continueEditDraft(s)">Continue
editing</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
</section>
<app-member-quick-add-dialog *ngIf="showQuickAdd"
(created)="onMemberQuickCreated($event)"
(cancelled)="showQuickAdd = false"></app-member-quick-add-dialog>
</div>
<app-member-quick-add-dialog *ngIf="showQuickAdd" (created)="onMemberQuickCreated($event)"
(cancelled)="showQuickAdd = false"></app-member-quick-add-dialog>
</div>
@@ -1,11 +1,32 @@
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.page-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; flex-wrap: wrap; }
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
.section-title { font-size: 1rem; font-weight: 600; margin: 0 0 0.75rem; }
.warn { background: #fff3cd; padding: 0.5rem 1rem; border-radius: 4px; margin-bottom: 1rem; }
.entry-row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; margin-bottom: 1rem; }
.entry-row label { display: flex; flex-direction: column; gap: 0.25rem; }
.entry-actions { display: flex; gap: 0.5rem; }
.anon-chip { padding: 0.25rem 0.5rem; background: #eee; border-radius: 4px; }
.reconcile { display: flex; gap: 1rem; align-items: flex-end; margin-top: 1rem; }
.reconcile .ok { color: green; font-weight: 600; }
.reconcile .bad { color: #c00; font-weight: 600; }
.edit-banner { display: flex; align-items: center; gap: 1rem; background: #fff3cd; border-left: 4px solid #f0a500; padding: 0.5rem 1rem; border-radius: 4px; margin-bottom: 1rem; font-weight: 500; }
.sessions-list { margin-top: 2rem; }
.ok { color: green; font-weight: 600; }
.bad { color: #c00; font-weight: 600; }
.empty { color: #6b7280; padding: 1rem 0; }
.lines-footer { margin-top: 0.5rem; color: #374151; font-weight: 500; }
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.4;
}
.badge--draft { background: #fef3c7; color: #92400e; }
.badge--submitted { background: #dcfce7; color: #166534; }
.badge--reconciled { background: #e2e8f0; color: #334155; }
@@ -87,6 +87,15 @@ export class OfferingSessionPageComponent implements OnInit {
});
}
// Already a Draft (e.g. a session reopened then left) — load it straight back in, no reopen.
continueEditDraft(s: OfferingSessionListItemDto): void {
if (s.status !== 'Draft') return;
this.api.getById(s.id).subscribe({
next: dto => this.loadIntoBuffer(dto),
error: (err: { error?: { message?: string } }) => alert(err?.error?.message ?? 'Load failed.'),
});
}
private loadIntoBuffer(dto: OfferingSessionDto): void {
this.editingSessionId = dto.id;
this.sessionDate = new Date(dto.sessionDate + 'T00:00:00');
@@ -110,6 +119,8 @@ export class OfferingSessionPageComponent implements OnInit {
this.buffer = []; this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
this.sessionDate = new Date();
this.checkDate();
// The reopened session is now a server-side Draft — refresh so its "Continue editing" appears.
this.loadSessions();
}
onMemberFilter(term: string): void {
@@ -130,6 +141,10 @@ export class OfferingSessionPageComponent implements OnInit {
this.selectedMemberId = null; this.selectedMemberName = null;
}
clearAnonymous(): void {
this.entry.isAnonymous = false;
}
addLine(): void {
if (this.entry.amount <= 0) return;
if (this.entry.paymentMethod === 'Check' && !this.entry.checkNumber) return;
@@ -221,5 +236,12 @@ export class OfferingSessionPageComponent implements OnInit {
};
}
private toIso(d: Date): string { return d.toISOString().slice(0, 10); }
// Format using LOCAL date components — NOT toISOString(), which converts to UTC and
// rolls the date forward a day for behind-UTC users when the Date carries an evening time.
private toIso(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
}
@@ -12,7 +12,7 @@
<div class="branding-section">
<div class="branding-content">
<div class="logo-container">
<img src="assets/rbj-logo.svg" alt="RBJ Logo" class="logo-image">
<img src="assets/images/ROLCC-Logo-Color.png" alt="ROLCC Logo" class="logo-image">
<div class="logo-text">
<h1>ROLCC AC</h1>
<span class="tagline">Church Management Portal</span>
@@ -5,7 +5,8 @@
<!-- TAB 1: Basic Info -->
<kendo-tabstrip-tab title="Basic Info" [selected]="true">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<div class="k-form k-form-vertical k-mt-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<kendo-formfield>
<kendo-label text="Legal First Name *"></kendo-label>
@@ -17,7 +18,7 @@
<kendo-textbox formControlName="lastName_en"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-formfield class="md:col-span-2">
<kendo-label text="Nick Name (Common Name)"></kendo-label>
<kendo-textbox formControlName="nickName" placeholder="e.g. Chris"></kendo-textbox>
</kendo-formfield>
@@ -63,6 +64,7 @@
</kendo-dropdownlist>
</kendo-formfield>
</div>
</div>
</ng-template>
</kendo-tabstrip-tab>
@@ -70,9 +72,10 @@
<!-- TAB 2: Contact -->
<kendo-tabstrip-tab title="Contact">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<div class="k-form k-form-vertical k-mt-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<kendo-formfield>
<kendo-formfield class="md:col-span-2">
<kendo-label text="Email"></kendo-label>
<kendo-textbox formControlName="email"></kendo-textbox>
</kendo-formfield>
@@ -87,7 +90,7 @@
<kendo-textbox formControlName="phoneHome"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-formfield class="md:col-span-2">
<kendo-label text="Address"></kendo-label>
<kendo-textbox formControlName="address"></kendo-textbox>
</kendo-formfield>
@@ -112,6 +115,7 @@
<kendo-textbox formControlName="country"></kendo-textbox>
</kendo-formfield>
</div>
</div>
</ng-template>
</kendo-tabstrip-tab>
@@ -119,7 +123,8 @@
<!-- TAB 3: Church Info -->
<kendo-tabstrip-tab title="Church Info">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<div class="k-form k-form-vertical k-mt-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<kendo-formfield>
<kendo-label text="Join Date"></kendo-label>
@@ -131,16 +136,17 @@
<kendo-datepicker formControlName="baptismDate"></kendo-datepicker>
</kendo-formfield>
<kendo-formfield>
<kendo-formfield class="md:col-span-2">
<kendo-label text="Baptism Church"></kendo-label>
<kendo-textbox formControlName="baptismChurch"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-formfield class="md:col-span-2">
<kendo-label text="Notes"></kendo-label>
<kendo-textarea formControlName="notes" [rows]="4"></kendo-textarea>
</kendo-formfield>
</div>
</div>
</ng-template>
</kendo-tabstrip-tab>
@@ -1,5 +1,6 @@
<kendo-dialog title="Add New User" (close)="onCancel()" [minWidth]="460" [width]="500">
<form [formGroup]="form" class="k-form k-form-vertical k-p-2">
<div class="grid grid-cols-1 gap-y-3">
<kendo-formfield>
<kendo-label text="Email *"></kendo-label>
@@ -8,14 +9,14 @@
<kendo-formerror *ngIf="form.get('email')?.errors?.['email']">Invalid email.</kendo-formerror>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-formfield>
<kendo-label text="Roles *"></kendo-label>
<kendo-multiselect formControlName="roles" [data]="roleOptions"
placeholder="Select roles"></kendo-multiselect>
<kendo-formerror>At least one role is required.</kendo-formerror>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-formfield>
<kendo-label text="Language"></kendo-label>
<kendo-dropdownlist formControlName="languagePreference"
[data]="langOptions" textField="text" valueField="value"
@@ -23,12 +24,13 @@
</kendo-dropdownlist>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-formfield>
<kendo-label text="Member ID (optional)"></kendo-label>
<kendo-numerictextbox formControlName="memberId" [format]="'0'"
placeholder="Link to a member record"></kendo-numerictextbox>
</kendo-formfield>
</div>
</form>
<kendo-dialog-actions>
@@ -1,5 +1,6 @@
<kendo-dialog title="Edit User" (close)="onCancel()" [minWidth]="460" [width]="500">
<form [formGroup]="form" class="k-form k-form-vertical k-p-2">
<div class="grid grid-cols-1 gap-y-3">
<kendo-formfield>
<kendo-label text="Email *"></kendo-label>
@@ -8,13 +9,13 @@
<kendo-formerror *ngIf="form.get('email')?.errors?.['email']">Invalid email.</kendo-formerror>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-formfield>
<kendo-label text="Roles *"></kendo-label>
<kendo-multiselect formControlName="roles" [data]="roleOptions"
placeholder="Select roles"></kendo-multiselect>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-formfield>
<kendo-label text="Language"></kendo-label>
<kendo-dropdownlist formControlName="languagePreference"
[data]="langOptions" textField="text" valueField="value"
@@ -22,11 +23,12 @@
</kendo-dropdownlist>
</kendo-formfield>
<div class="k-d-flex k-align-items-center k-gap-2 k-mt-4">
<div class="flex items-center gap-2">
<input kendoCheckBox type="checkbox" formControlName="isActive" id="isActiveCheck" />
<kendo-label for="isActiveCheck" text="Account Active"></kendo-label>
</div>
</div>
</form>
<kendo-dialog-actions>
@@ -16,10 +16,10 @@
<aside class="sidebar" [class.collapsed]="sidebarCollapsed">
<div class="sidebar-header">
<div class="logo-section">
<img src="assets/rbj-logo.svg" alt="RBJ Logo" class="logo-image">
<img src="assets/images/ROLCC-Logo-Color.png" alt="ROLCC Logo" class="logo-image">
<div class="logo-text" *ngIf="!sidebarCollapsed">
<h2>ROLCC AC</h2>
<span class="tagline">Escrow Portal</span>
<span class="tagline">Church Portal</span>
</div>
</div>
<button class="sidebar-toggle" (click)="toggleSidebar()" title="Toggle sidebar">
@@ -1,7 +1,7 @@
<div class="token-verification-container">
<div class="verification-card">
<div class="logo-section">
<img src="assets/rbj-logo.svg" alt="RBJ Logo" class="logo">
<img src="assets/images/ROLCC-Logo-Color.png" alt="ROLCC Logo" class="logo">
</div>
<div class="verification-content">
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+19
View File
@@ -0,0 +1,19 @@
/*
* Tailwind v4 — utilities + theme only. NO preflight: Kendo UI owns base element
* styling and styles.scss already ships its own reset. Loading Tailwind's base reset
* would fight Kendo.
*
* theme -> kept in its own layer (design tokens / CSS vars only, no output by itself)
* utilities -> imported UNLAYERED so layout utilities (grid/col-span/gap/flex) reliably
* win over Kendo's unlayered CDN theme when they ever land on the same node.
*
* Used by the shared form-layout convention: a neutral wrapper <div> carries
* grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3
* and full-width fields use md:col-span-2.
*/
@layer theme;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css";
/* Explicit source scanning (auto-detection also covers these). */
@source "./";
+4 -4
View File
@@ -1315,12 +1315,12 @@ assets/i18n/zh-TW.json
## 7. 開發階段規劃 (Roadmap)
### Phase 0 — 技術基礎 & DevOps (23 週)
- [ ] Azure VM 建置 + Docker 環境
- [X] Azure VM 建置 + Docker 環境
- [ ] Gitea + Jenkins 啟動,CI pipeline 通
- [ ] Angular + Capacitor 專案骨架(含 ngx-translate en/zh-TW
- [ ] ASP.NET Core API 骨架 + EF Core 初始 Migration
- [ ] 認證系統(JWT + Refresh Token + ASP.NET Identity
- [ ] RBAC 框架(角色 + Ministry Scope middleware
- [X] ASP.NET Core API 骨架 + EF Core 初始 Migration
- [X] 認證系統(JWT + Refresh Token + ASP.NET Identity
- [X] RBAC 框架(角色 + Ministry Scope middleware
- [ ] Audit Log 基礎建設
- [ ] Mobile-first UI 元件庫設定(底部導覽、touch target 規範)