linguagem
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
test_nif.js,1777390754764,b50e0b4fe7732186ea056d183a0f2bbe7026cf79d384ce66391f746fe6969af6
|
||||
temp_script.jsx,1778062694175,e61487a0533f953dd5040c3f8bfba051119ee86ea855d2baf353bbe20f7abd86
|
||||
sw.js,1778065253229,20540da27a8005d1e7a54a576607c687217feb037aedb26e5d8dab53d69deb34
|
||||
sw.js,1778230406112,5d7efd8fe9879b039b8a39bdf56cc6b7486b7fc4e27776aba134972a26de2523
|
||||
style.css,1770114976862,4c2e2686b637f6f2f060298dfbabf690219284ff4c5c027711c5b443dde07332
|
||||
script.js,1776937115126,4b08e5f41663ef287d352039798448e36a3c52c60014d58f7ed31471dab4066d
|
||||
manifest.json,1770114976862,eb6e5b596d2a562026e361e5ee5bd1f4c3fc94a5e3b8cfc9d8761c6d21b2b991
|
||||
firebase.js,1776936516051,37feb64e428313ec44afdabc4a2a348f29aae1ce0893c8099205750b1b5faf87
|
||||
firebase,1777902366124,9d8f53c2037285ddb56fad26e9a581980370cec0dae5bfae0e91e5a87e8b96b2
|
||||
diff.txt,1778227426505,5c43e21897b2247e203b29b2a1322bb7e4e24ffb53ffa8c233ced1a002370548
|
||||
RELATORIO_TECNICO.md,1778058174796,fad35f12b1f2d062f72e7a448bb643fd3cfdd24423eaacc14cbbb20172ead7be
|
||||
README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce3d700605
|
||||
.vscode/settings.json,1778056161665,3a247752ccf28f259e2e604bf44311ab91b6db3864b120e08f2823951d1c55d8
|
||||
.git/index,1778067817139,b6a4caa468a35715a92747090f32166eadf3447788dbe8a15ca14f407d77c7f5
|
||||
.git/index,1778142319280,ab76dcf6508b9c5d11367962c2914ffa97326380fae04b0bc644306e2fd2f52f
|
||||
.git/description,1773160274654,3cdc7b6a29de07f63b76d16b9911d93468000346945f759d4f6456660b5c113b
|
||||
.git/config,1773914677241,cb4cd1c7c28ac13d2dccf79f667245402cf551c998ba1f6b58abc28f3ae11e7f
|
||||
.git/ORIG_HEAD,1777392979044,2612c449de4f930bfb197ddb13780ca77cbb7bf6db7e91493d412b634ddcdebd
|
||||
.git/HEAD,1773160809135,a39dc51e21d1523cdef2091e7c7ab30a33ad42a7cd5da1f45139746e5c24b667
|
||||
.git/COMMIT_EDITMSG,1778067817140,6df556362d9eb5101e618d025d58a67cca69a740780977ebdaf2579be85ce022
|
||||
.git/refs/remotes/origin/main,1778067817533,c6caea8a10e8a97102165c36d3bdde2925b4777bea432affa7702c69316113bf
|
||||
.git/COMMIT_EDITMSG,1778142319280,6df556362d9eb5101e618d025d58a67cca69a740780977ebdaf2579be85ce022
|
||||
.git/refs/remotes/origin/main,1778142319634,36078041c052a72d91fd289dcd7f4ec8a7f2ca2a77582b9a53ab10fa0789fbe4
|
||||
.git/refs/remotes/origin/HEAD,1773830493701,0f5d56efe56c5dcabb387d965aad58d0f60a3b7485cb9b04bef04b93bebf911e
|
||||
.git/refs/heads/main,1778067817141,c6caea8a10e8a97102165c36d3bdde2925b4777bea432affa7702c69316113bf
|
||||
.git/refs/heads/main,1778142319281,36078041c052a72d91fd289dcd7f4ec8a7f2ca2a77582b9a53ab10fa0789fbe4
|
||||
.git/objects/fd/3d3838a9118dc446e6ab65d38a5ce1747fd0d6,1777393032091,b7fcc251a3edcfc6eb1d7a0ea70a10d16f297520eb5e95c822ab5f754da8dc4a
|
||||
.git/objects/f7/30f952945d24a8d4137d1c591d7477ee96c1bd,1778142319279,13078d7dc1d56d7c137032550b76a68b209b664f988963a49b18b4d6226fe3c0
|
||||
.git/objects/f6/b7a98471bb9c718c8091a62fa65243aceb92b7,1778067817140,12c318e32181abf048c4811d48552676bff899b5172e58d11fd9136ae98c2c71
|
||||
.git/objects/f6/73a71b7a275609030462c0278586d61e5f3a00,1773830457776,6ef355c279049956337049bd863d18f205f75ec1bcaee8c82bf26a7d2a65af72
|
||||
.git/objects/f4/577dec9e03ee9efa3af7ee05d60576b07f8d99,1773161014724,c78b5a6ceff9eb62984db81b7041efbdcac774d45a96d7536ede20a9a6ba10c5
|
||||
@@ -36,6 +37,7 @@ README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce
|
||||
.git/objects/b2/d3930d99b8f06b5d0ae445cc28cd2a4af0da24,1778067817138,e9d22b7f6ef7f12457135884486154e22b6c283f056db3968907d4585fda8ec2
|
||||
.git/objects/b1/23a037ff8edd8fe29dc828726a53f9b55dcc31,1773161195361,c118b3ce9a20c3b4aa540144f17e7dcc1bc26ff0b576c6e00451941e200feee9
|
||||
.git/objects/aa/64354c06769190bc3e113bd7ee4dfb0bfcdad1,1773830457796,31833bc03904dec3c2a34a49028c56042ee348df266b859968414bc009c83f7f
|
||||
.git/objects/a7/f8e39aae601c1c6abba5111b8b248cfc1a276d,1778142319281,7125a63b43e0fee779f311bf76e4ffdd2386beb24327d9bd949cda8b9af52644
|
||||
.git/objects/a5/c46d963a0c127d421044eb6e50f1b3c8271d95,1777903218134,32190169472d3bc7ae8b307521f604f6fcdeed486694f0deffa2e9e55dc72e9a
|
||||
.git/objects/a0/de46ff5465329041fcd659cf66cadca4415824,1776937200146,43fc08e82b2fabcf91b8670535d21d353a189c5cbd6fe234369b05fead5c8e5e
|
||||
.git/objects/9e/71203cb36f10248f9fa89f1d11f8335ed55be2,1773830457795,c6ee24879edd639035190699f85cf3cdfeb1a8926218f94b9152470d9e75e25d
|
||||
@@ -67,6 +69,7 @@ README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce
|
||||
.git/objects/44/85476e52d470cdaba6e18db9df7591fceb0b13,1776183305294,2e7939525d8c7c114190b2aaeaa1cd712ac0d4153c4222c0c6f2c49a4aeb3e1a
|
||||
.git/objects/43/83d1d591ec9471fd88ffef009891025b35c0b8,1778067817124,a7194c7f232461447a126e58dabcdc62470788fa032b4a36070ac255b0c52cf6
|
||||
.git/objects/40/4bcf86370b3aa54e5c09e50a7be47241fe63ee,1773830230299,f9bae290124afebf31f114d71dc73d407a04a248fff87a729abfa68e807e6cf9
|
||||
.git/objects/3d/c2410332e1854dd930aadb3e34152a1dfd2b7d,1778142319262,571f3e6c73b09f5ddce8e0276cfe6bd0e4c23c2675d13aebf85e8cd76cec5b3a
|
||||
.git/objects/3b/cafc02b8f3692fbb5b6d93debe13bb50563c2a,1776937200150,70a72e7716314bf0c914dd3d802a5a0ba4c93a23c179aaa86fb8c660f039e344
|
||||
.git/objects/38/465e5ae301b3ddefabfe22b188c4fee52182c0,1773161195398,dbcf76217184aff41336a8f6530dc63b073bb4235b0fec701dfa33df27a0b402
|
||||
.git/objects/33/28ec86365d3f1a608e8dd533291ff447c97d00,1777997762141,34991e7e8382ce6d32b8a5a1ee47d64613523696d9a8b5ae59cbf5375155559d
|
||||
@@ -87,6 +90,8 @@ README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce
|
||||
.git/objects/23/cec1dc9936a6d9be11078377c6a7685b26373f,1776183305295,d709b165a1199d66da18ba51afe1471aa7b068d4066284bf28d333f09e0c9670
|
||||
.git/objects/1f/96a9bfae61d4e97fc9f0375975e89f031ce971,1777542176253,10e365b22153e0f123b7086751a36b6779ce2368f721a3edffed8be8b92c4dba
|
||||
.git/objects/1e/df059fdd108ea56915dfccc9a58ad4170d580b,1778067817119,dcaad430c4d88918e61ac956c09211ed40896ae36b5285de662d2cc13db50355
|
||||
.git/objects/1b/95650abf8d99d02977de5d04e6dfe3e07b134a,1778142319279,a7e079c7dcf41cc8468bc5f8ce72c6501a952c6a0eb14dca080386f687b460b2
|
||||
.git/objects/1a/fa1ed665e30605cae8462faf9d0fb8ad15bc18,1778142319265,82370cdcc2131fd6e09cb7ea2961c932342c6264cc1e345f27c43f1122390a08
|
||||
.git/objects/1a/6f0a583386ccf1152060c78f3237e4b928ebab,1777903218122,3b445ff37e72f864c5ce690f3b9e417ddfaee3e33dca1ad27390fcdc5e8c8e8d
|
||||
.git/objects/15/5422b0a09c128c529df656111fa3bf4a810999,1776183305292,f3973efacb5d450d6636f40464edc7361a5367cf15fa47ec6f6fdc0958b1548b
|
||||
.git/objects/11/0a88a7582625e48e2eddd5e18dfc7e2f6ecbd7,1778067817118,039e21f346feb3301f5fffb0ec308a4d5f5e4a2cc1a39563e59e12ea0b57a74c
|
||||
@@ -95,10 +100,10 @@ README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce
|
||||
.git/objects/0b/fc5efae1d163add058f1940feea6649f5da1b1,1777393032074,f19ecb9e3799eb6cd2f62e145b429f8c7c48fe8ffda704c49d6d925e9818c000
|
||||
.git/objects/04/11accbb7c7608ce7cc7a296cdaaaf9c611fd28,1778067817122,106c0ea1a1d8ee8d73a0e48241240894ee4cd3269e40c191b8821a897d3a4a25
|
||||
.git/objects/00/0c1cd721b99d14281c3724f5488b040c152515,1777997762142,7d131dd9c4c33d6e017428809d3b927f369ee8fa38c7d6ae79d01bc10561e22b
|
||||
.git/logs/HEAD,1778067817141,7bdcbeaa71468c78478906ffb453c4c7c65b8ddc4f706f53fae509ff64405372
|
||||
.git/logs/refs/remotes/origin/main,1778067817533,8a9c24916f99593929644967d9a8f3e00a810b8c014df0469d7809b4b4ff29e8
|
||||
.git/logs/HEAD,1778142319281,4230a25743774efc5dc7eb3f93703717d798e891ecb21cb6cb27e9db7e322eae
|
||||
.git/logs/refs/remotes/origin/main,1778142319635,0972562652fc6a13ebbfdea2f22a172e20a350c3ac55e979dc745b11499dc0ab
|
||||
.git/logs/refs/remotes/origin/HEAD,1773830493702,1eba2cff5035849e216a15d3b6013593fa5ef345a8d76bb2881d83b3cb247576
|
||||
.git/logs/refs/heads/main,1778067817141,7bdcbeaa71468c78478906ffb453c4c7c65b8ddc4f706f53fae509ff64405372
|
||||
.git/logs/refs/heads/main,1778142319281,4230a25743774efc5dc7eb3f93703717d798e891ecb21cb6cb27e9db7e322eae
|
||||
.git/info/exclude,1773160274653,a362e375cc3330f10d115cfeb0f90a325219d80a764d57e2c4873f78d1d0b4f5
|
||||
.git/hooks/update.sample,1773160274656,2b0a4f42fa30a128b46ad80e89c1f73b89d58b8abb9e92aee1c35625baccb584
|
||||
.git/hooks/sendemail-validate.sample,1773160274654,4d0768bc11017be6b99d4bb4d34b4c8b2fd7ae8a93d42727591afb6737577db2
|
||||
@@ -114,5 +119,6 @@ README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce
|
||||
.git/hooks/fsmonitor-watchman.sample,1773160274655,d366d691e33458260d77c44be36050a3faf0aa12760955cc8ca85ee88389c400
|
||||
.git/hooks/commit-msg.sample,1773160274654,4df962ba3955944bec38b211351c73f083d7b0e5360a5d3d76a49548e7314f9e
|
||||
.git/hooks/applypatch-msg.sample,1773160274655,91b94f5feaf0e4d2e6e7808a9188384a4300adf024fa24c48547ee87c64d6558
|
||||
.git/FETCH_HEAD,1778141708711,df42fc98d5f625a624c1d4a1cac368b5d9d92a4ed6841b5fc530e12a8185c643
|
||||
index.html,1778141854716,8c8a2ce5f2e4811c1010aa00979e793cc45bb70f7990e4bb981009a2fb2e3292
|
||||
.git/FETCH_HEAD,1778233047619,6239b093503ca2be47debac6c929f0e9c95a70f288976e1db864811742a15cb9
|
||||
temp_script.jsx,1778231961660,0300cccadb4be44eb475ffa5f873de59003c261b28cd6d7ad79a352903a30c2b
|
||||
index.html,1778231918196,c7de50f6e2f8f93ae1479a7a3e62822f785c8a3b1334daf9f350a50da728d6de
|
||||
|
||||
374
diff.txt
Normal file
374
diff.txt
Normal file
@@ -0,0 +1,374 @@
|
||||
--- index.html 2026-05-07 09:17:34
|
||||
+++ temp_script.jsx 2026-05-08 09:01:25
|
||||
@@ -40,7 +40,6 @@
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
-
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
@@ -100,41 +99,10 @@
|
||||
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
|
||||
MessageCircle, Paperclip, Send
|
||||
} from 'lucide-react';
|
||||
-
|
||||
import { app } from './firebase.js';
|
||||
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js';
|
||||
import { getDatabase, ref, push, set, onValue, remove, update } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
|
||||
|
||||
- class ErrorBoundary extends React.Component {
|
||||
- constructor(props) {
|
||||
- super(props);
|
||||
- this.state = { hasError: false, error: null, errorInfo: null };
|
||||
- }
|
||||
- static getDerivedStateFromError(error) {
|
||||
- return { hasError: true };
|
||||
- }
|
||||
- componentDidCatch(error, errorInfo) {
|
||||
- this.setState({ error, errorInfo });
|
||||
- console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
- }
|
||||
- render() {
|
||||
- if (this.state.hasError) {
|
||||
- return (
|
||||
- <div style={{ padding: '20px', backgroundColor: '#fee2e2', color: '#991b1b', fontFamily: 'sans-serif', height: '100vh' }}>
|
||||
- <h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Algo correu mal (Erro na Aplicação)</h1>
|
||||
- <pre style={{ marginTop: '20px', whiteSpace: 'pre-wrap', backgroundColor: '#fef2f2', padding: '15px', border: '1px solid #f87171' }}>
|
||||
- {this.state.error && this.state.error.toString()}
|
||||
- <br />
|
||||
- {this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||
- </pre>
|
||||
- <button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>Recarregar Página</button>
|
||||
- </div>
|
||||
- );
|
||||
- }
|
||||
- return this.props.children;
|
||||
- }
|
||||
- }
|
||||
-
|
||||
const auth = getAuth(app);
|
||||
const db = getDatabase(app);
|
||||
|
||||
@@ -341,23 +309,12 @@
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
- const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
- if (isLoading) return;
|
||||
-
|
||||
- setError('');
|
||||
- setIsLoading(true);
|
||||
-
|
||||
const success = await onLogin(email, password);
|
||||
-
|
||||
- setIsLoading(false);
|
||||
if (!success) {
|
||||
setError('Email ou Palavra-passe incorreta');
|
||||
- setTimeout(() => {
|
||||
- setError('');
|
||||
- }, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -403,15 +360,8 @@
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
- <button type="submit" disabled={isLoading} className={`w-full ${isLoading ? 'bg-blue-400 dark:bg-blue-600/50 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/30'} text-white font-bold py-3 rounded-lg transition-colors flex justify-center items-center gap-2`}>
|
||||
- {isLoading ? (
|
||||
- <>
|
||||
- <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
- A entrar...
|
||||
- </>
|
||||
- ) : (
|
||||
- 'Entrar'
|
||||
- )}
|
||||
+ <button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
||||
+ Entrar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -490,7 +440,7 @@
|
||||
userId = 'admin_001';
|
||||
} else {
|
||||
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
||||
- if (residentUser && (password === residentUser.password || password === '1234')) {
|
||||
+ if (residentUser && (password === residentUser.contact || password === '1234')) {
|
||||
role = residentUser.role || 'morador';
|
||||
userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
|
||||
userId = residentUser.id || userId;
|
||||
@@ -550,17 +500,7 @@
|
||||
return onValue(ref(db, path), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (data) {
|
||||
- let parsed = Object.entries(data).map(([id, val]) => {
|
||||
- if (path === 'faturas' && val.status === 'Em Validação') {
|
||||
- return { id, ...val, status: 'Pago' };
|
||||
- }
|
||||
- return { id, ...val };
|
||||
- });
|
||||
-
|
||||
- if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
|
||||
- parsed = parsed.filter(item => item.moradorId === currentUserId);
|
||||
- }
|
||||
-
|
||||
+ let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
|
||||
if (sortFunc) parsed = parsed.sort(sortFunc);
|
||||
setter(parsed);
|
||||
} else {
|
||||
@@ -781,8 +721,7 @@
|
||||
try {
|
||||
if (editingItem) {
|
||||
const residentRef = ref(db, `condominos/${editingItem.id}`);
|
||||
- const updatedData = {
|
||||
- ...editingItem,
|
||||
+ await set(residentRef, {
|
||||
unit: formData.unit || '',
|
||||
name: formData.name || '',
|
||||
contact: formData.contact || '',
|
||||
@@ -790,11 +729,7 @@
|
||||
status: formData.status || 'Pago',
|
||||
pending: Number(formData.pending) || 0,
|
||||
role: formData.role || 'morador'
|
||||
- };
|
||||
- if (formData.password) {
|
||||
- updatedData.password = formData.password;
|
||||
- }
|
||||
- await set(residentRef, updatedData);
|
||||
+ });
|
||||
showNotification(`Condómino ${formData.name} atualizado`);
|
||||
} else {
|
||||
const residentsListRef = ref(db, 'condominos');
|
||||
@@ -804,7 +739,6 @@
|
||||
name: formData.name || '',
|
||||
contact: formData.contact || '',
|
||||
email: formData.email || '',
|
||||
- password: formData.password || '1234',
|
||||
status: formData.status || 'Pago',
|
||||
pending: Number(formData.pending) || 0,
|
||||
role: formData.role || 'morador'
|
||||
@@ -864,7 +798,7 @@
|
||||
}
|
||||
try {
|
||||
const newIssueRef = push(ref(db, 'manutencao'));
|
||||
- await set(newIssueRef, { ...formData, moradorId: currentUserId });
|
||||
+ await set(newIssueRef, { ...formData });
|
||||
|
||||
sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
|
||||
if (userRole !== 'admin') {
|
||||
@@ -918,17 +852,10 @@
|
||||
|
||||
const handlePayFatura = async (fatura) => {
|
||||
try {
|
||||
- await set(ref(db, `faturas/${fatura.id}/status`), 'Pago');
|
||||
-
|
||||
- const morador = residents.find(r => r.id === fatura.moradorId);
|
||||
- if (morador) {
|
||||
- let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
|
||||
- if (newPending < 0) newPending = 0;
|
||||
- await set(ref(db, `condominos/${morador.id}/pending`), newPending);
|
||||
- }
|
||||
- sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi concluído!`, 'success', fatura.moradorId);
|
||||
- sendSystemNotification(`Pagamento registado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin');
|
||||
- showNotification("Pagamento efetuado com sucesso!", "success");
|
||||
+ await set(ref(db, `faturas/${fatura.id}/status`), 'Em Validação');
|
||||
+ sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi submetido. Aguarda validação.`, 'info', fatura.moradorId);
|
||||
+ sendSystemNotification(`Comprovativo recebido da fração ${fatura.fracao}.`, 'info', 'admin');
|
||||
+ showNotification("Comprovativo enviado! A aguardar validação do administrador.", "success");
|
||||
} catch (error) {
|
||||
console.error("Erro ao pagar fatura:", error);
|
||||
showNotification("Erro ao processar pagamento.", "error");
|
||||
@@ -979,8 +906,7 @@
|
||||
const bookingData = {
|
||||
...formData,
|
||||
facilityName: facilityNames[formData.facility],
|
||||
- status: 'Confirmado',
|
||||
- moradorId: currentUserId
|
||||
+ status: 'Confirmado'
|
||||
};
|
||||
|
||||
const newBookingRef = push(ref(db, 'reservas'));
|
||||
@@ -1289,74 +1215,7 @@
|
||||
|
||||
const ProfileView = ({ theme, setTheme }) => {
|
||||
const [activeSection, setActiveSection] = useState('personal');
|
||||
-
|
||||
- const isMorador = userRole !== 'admin';
|
||||
- const [formData, setFormData] = useState({
|
||||
- name: 'A carregar...',
|
||||
- role: '...',
|
||||
- email: '',
|
||||
- contact: '',
|
||||
- address: ''
|
||||
- });
|
||||
|
||||
- useEffect(() => {
|
||||
- if (isMorador) {
|
||||
- const currentUserData = residents.find(r => r.id === currentUserId) || {};
|
||||
- setFormData({
|
||||
- name: currentUserData.name || currentUserName || '',
|
||||
- role: `Fração ${currentUserData.unit || 'N/A'}`,
|
||||
- email: currentUserData.email || '',
|
||||
- contact: currentUserData.contact || '',
|
||||
- address: 'Morada do Condomínio'
|
||||
- });
|
||||
- } else {
|
||||
- const adminRef = ref(db, 'configuracoes/admin_profile');
|
||||
- const unsub = onValue(adminRef, (snapshot) => {
|
||||
- if (snapshot.exists()) {
|
||||
- setFormData(snapshot.val());
|
||||
- } else {
|
||||
- setFormData({
|
||||
- name: 'Administrador do Condomínio',
|
||||
- role: 'Síndico / Gestor',
|
||||
- email: 'admin@mycondominium.pt',
|
||||
- contact: '+351 912 345 678',
|
||||
- address: 'Rua das Flores, nº 123, Escritório 2B'
|
||||
- });
|
||||
- }
|
||||
- });
|
||||
- return () => unsub();
|
||||
- }
|
||||
- }, [residents, currentUserId, userRole, currentUserName, isMorador]);
|
||||
-
|
||||
- const handleChange = (field, value) => {
|
||||
- setFormData(prev => ({ ...prev, [field]: value }));
|
||||
- };
|
||||
-
|
||||
- const handleSave = async () => {
|
||||
- if (isMorador) {
|
||||
- const currentUserData = residents.find(r => r.id === currentUserId);
|
||||
- if (currentUserData && currentUserData.id) {
|
||||
- try {
|
||||
- await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email);
|
||||
- await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact);
|
||||
- showNotification('Dados atualizados com sucesso!', 'success');
|
||||
- sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
|
||||
- } catch (error) {
|
||||
- console.error("Erro ao guardar os dados:", error);
|
||||
- showNotification('Erro ao guardar os dados.', 'error');
|
||||
- }
|
||||
- }
|
||||
- } else {
|
||||
- try {
|
||||
- await set(ref(db, 'configuracoes/admin_profile'), formData);
|
||||
- showNotification('Alterações guardadas com sucesso!', 'success');
|
||||
- } catch (error) {
|
||||
- console.error("Erro ao guardar perfil admin:", error);
|
||||
- showNotification('Erro ao guardar as alterações.', 'error');
|
||||
- }
|
||||
- }
|
||||
- };
|
||||
-
|
||||
return (
|
||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
|
||||
<div className="flex flex-col md:flex-row h-full">
|
||||
@@ -1405,16 +1264,19 @@
|
||||
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">Dados Pessoais</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
- <InputGroup label="Nome Completo" value={formData.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} />
|
||||
- <InputGroup label={isMorador ? "Fração" : "Cargo"} value={formData.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} />
|
||||
+ <InputGroup label="Nome Completo" value="Administrador do Condomínio" disabled />
|
||||
+ <InputGroup label="Cargo" value="Síndico / Gestor" disabled />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
- <InputGroup label="Email" value={formData.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
|
||||
- <InputGroup label="Telefone" value={formData.contact} onChange={(e) => handleChange('contact', e.target.value)} />
|
||||
+ <InputGroup label="Email" value="admin@mycondominium.pt" type="email" />
|
||||
+ <InputGroup label="Telefone" value="+351 912 345 678" />
|
||||
</div>
|
||||
- <InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={formData.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} />
|
||||
+ <InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
|
||||
<div className="flex justify-end mt-6">
|
||||
- <button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
+ <button onClick={() => {
|
||||
+ showNotification('Alterações guardadas com sucesso!', 'success');
|
||||
+ sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
|
||||
+ }} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
Guardar Alterações
|
||||
</button>
|
||||
</div>
|
||||
@@ -2038,6 +1900,10 @@
|
||||
>
|
||||
Pagar
|
||||
</button>
|
||||
+ ) : fatura.status === 'Em Validação' ? (
|
||||
+ <span className="text-orange-500 text-xs font-bold flex items-center justify-center gap-1">
|
||||
+ <Clock size={14} /> Em Validação
|
||||
+ </span>
|
||||
) : (
|
||||
<span className="text-slate-400 text-xs font-bold flex items-center justify-center gap-1">
|
||||
<CheckCircle size={14} /> Pago
|
||||
@@ -2090,32 +1956,12 @@
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Diário Financeiro</h3>
|
||||
<span className="text-xs bg-slate-100 dark:bg-dark-bg text-slate-600 dark:text-slate-400 px-2 py-1 rounded-full">{finances.length} movimentos</span>
|
||||
</div>
|
||||
- <div className="flex items-center gap-3">
|
||||
- {finances.length === 0 && (
|
||||
- <button
|
||||
- onClick={async () => {
|
||||
- try {
|
||||
- for (const item of INITIAL_FINANCES) {
|
||||
- await set(push(ref(db, 'financas')), item);
|
||||
- }
|
||||
- showNotification("Dados de exemplo restaurados com sucesso!", "success");
|
||||
- } catch (error) {
|
||||
- console.error("Erro ao restaurar:", error);
|
||||
- showNotification("Erro ao restaurar.", "error");
|
||||
- }
|
||||
- }}
|
||||
- className="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors shadow-sm flex items-center gap-2"
|
||||
- >
|
||||
- <Wrench size={16} /> Restaurar Base de Dados
|
||||
- </button>
|
||||
- )}
|
||||
- <button
|
||||
- onClick={() => handleOpenModal('finance')}
|
||||
- className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
|
||||
- >
|
||||
- <Plus size={18} /> Novo Registo
|
||||
- </button>
|
||||
- </div>
|
||||
+ <button
|
||||
+ onClick={() => handleOpenModal('finance')}
|
||||
+ className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
|
||||
+ >
|
||||
+ <Plus size={18} /> Novo Registo
|
||||
+ </button>
|
||||
</div>
|
||||
<div className="overflow-auto flex-1">
|
||||
<table className="w-full text-sm text-left">
|
||||
@@ -2344,11 +2190,8 @@
|
||||
<InputGroup label="Fração" name="unit" value={formData.unit || ''} onChange={handleInputChange} placeholder="Ex: 1º Esq" required />
|
||||
<InputGroup label="Nome Completo" name="name" value={formData.name || ''} onChange={handleInputChange} placeholder="Nome do proprietário" required />
|
||||
<InputGroup label="Email" type="email" name="email" value={formData.email || ''} onChange={handleInputChange} placeholder="email@exemplo.com" />
|
||||
+ <InputGroup label="Contacto" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
- <InputGroup label="Telemóvel" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" required />
|
||||
- <InputGroup label="Palavra-passe" type="text" name="password" value={formData.password || ''} onChange={handleInputChange} placeholder={editingItem ? "Deixar em branco para manter" : "1234"} required={!editingItem} />
|
||||
- </div>
|
||||
- <div className="grid grid-cols-2 gap-4">
|
||||
<InputGroup label="Estado" name="status" value={formData.status || 'Pago'} onChange={handleInputChange} options={[{ value: 'Pago', label: 'Pago' }, { value: 'Pendente', label: 'Pendente' }, { value: 'Atrasado', label: 'Atrasado' }]} />
|
||||
<InputGroup label="Valor Pendente (€)" type="number" name="pending" value={formData.pending || 0} onChange={handleInputChange} />
|
||||
</div>
|
||||
@@ -2492,12 +2335,8 @@
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
- root.render(
|
||||
- <ErrorBoundary>
|
||||
- <App />
|
||||
- </ErrorBoundary>
|
||||
- );
|
||||
+ root.render(<App />);
|
||||
</script>
|
||||
<!-- Firebase configs moved to top in React Module -->
|
||||
</body>
|
||||
-</html>
|
||||
\ No newline at end of file
|
||||
+</html>
|
||||
119
index.html
119
index.html
@@ -18,9 +18,8 @@
|
||||
dark: {
|
||||
bg: '#0f172a',
|
||||
surface: '#1e293b',
|
||||
card: '#334155',
|
||||
border: '#475569',
|
||||
text: '#f1f5f9',
|
||||
border: '#334155',
|
||||
card: '#1e293b',
|
||||
mute: '#94a3b8'
|
||||
}
|
||||
}
|
||||
@@ -29,6 +28,24 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Google Translate Script -->
|
||||
<script type="text/javascript">
|
||||
function googleTranslateElementInit() {
|
||||
new google.translate.TranslateElement({pageLanguage: 'pt', includedLanguages: 'pt,en,es,fr', autoDisplay: false}, 'google_translate_element');
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
|
||||
|
||||
<style>
|
||||
/* Esconder a barra e o widget do Google Translate nativos */
|
||||
.skiptranslate iframe, .goog-te-banner-frame { display: none !important; }
|
||||
body { top: 0px !important; }
|
||||
#google_translate_element { display: none !important; }
|
||||
.goog-tooltip { display: none !important; }
|
||||
.goog-tooltip:hover { display: none !important; }
|
||||
.goog-text-highlight { background-color: transparent !important; border: none !important; box-shadow: none !important; }
|
||||
</style>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -490,7 +507,7 @@
|
||||
userId = 'admin_001';
|
||||
} else {
|
||||
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
||||
if (residentUser && (password === residentUser.password || password === '1234')) {
|
||||
if (residentUser && (password === residentUser.password || (!residentUser.password && password === residentUser.contact) || password === '1234')) {
|
||||
role = residentUser.role || 'morador';
|
||||
userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
|
||||
userId = residentUser.id || userId;
|
||||
@@ -735,8 +752,12 @@
|
||||
} else if (type === 'emitir_fatura') {
|
||||
setFormData(initialFaturaForm);
|
||||
} else if (type === 'booking') {
|
||||
const baseForm = initialBookingForm;
|
||||
const baseForm = { ...initialBookingForm };
|
||||
if (defaultFacility) baseForm.facility = defaultFacility;
|
||||
|
||||
// Preenche sempre o nome do utilizador logado por defeito
|
||||
baseForm.resident = currentUserName;
|
||||
|
||||
setFormData(baseForm);
|
||||
}
|
||||
};
|
||||
@@ -1332,6 +1353,43 @@
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' });
|
||||
const handlePasswordChange = (field, value) => {
|
||||
setPasswordData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSavePassword = async () => {
|
||||
if (passwordData.new !== passwordData.confirm) {
|
||||
showNotification('As novas palavras-passe não coincidem.', 'error');
|
||||
return;
|
||||
}
|
||||
if (passwordData.new.length < 4) {
|
||||
showNotification('A nova palavra-passe deve ter pelo menos 4 caracteres.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMorador) {
|
||||
const currentUserData = residents.find(r => r.id === currentUserId);
|
||||
const currentPassword = currentUserData.password || currentUserData.contact || '1234';
|
||||
if (passwordData.current !== currentPassword) {
|
||||
showNotification('A palavra-passe atual está incorreta.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new);
|
||||
showNotification('Palavra-passe alterada com sucesso!', 'success');
|
||||
setPasswordData({ current: '', new: '', confirm: '' });
|
||||
sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin');
|
||||
} catch (error) {
|
||||
console.error("Erro ao alterar palavra-passe:", error);
|
||||
showNotification('Erro ao alterar a palavra-passe.', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification('A conta de administrador usa o Firebase Auth para gerir passwords.', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isMorador) {
|
||||
const currentUserData = residents.find(r => r.id === currentUserId);
|
||||
@@ -1438,16 +1496,13 @@
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" />
|
||||
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" value={passwordData.current} onChange={(e) => handlePasswordChange('current', e.target.value)} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InputGroup label="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" />
|
||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
|
||||
<InputGroup label="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" value={passwordData.new} onChange={(e) => handlePasswordChange('new', e.target.value)} />
|
||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" value={passwordData.confirm} onChange={(e) => handlePasswordChange('confirm', e.target.value)} />
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button onClick={() => {
|
||||
showNotification('Segurança atualizada com sucesso!', 'success');
|
||||
sendSystemNotification('Um utilizador alterou a palavra-passe.', 'info', 'admin');
|
||||
}} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
<button onClick={handleSavePassword} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
Atualizar Segurança
|
||||
</button>
|
||||
</div>
|
||||
@@ -1504,6 +1559,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">Idioma da Aplicação</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ code: 'pt', label: 'Português', flag: '🇵🇹' },
|
||||
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
||||
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr', label: 'Français', flag: '🇫🇷' }
|
||||
].map(lang => {
|
||||
const match = document.cookie.match(/googtrans=\/pt\/([a-z]{2})/);
|
||||
const activeLang = match ? match[1] : 'pt';
|
||||
const isActive = activeLang === lang.code;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lang.code}
|
||||
onClick={() => {
|
||||
if(lang.code === 'pt') {
|
||||
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + window.location.hostname + "; path=/;";
|
||||
} else {
|
||||
document.cookie = `googtrans=/pt/${lang.code}; path=/;`;
|
||||
document.cookie = `googtrans=/pt/${lang.code}; domain=${window.location.hostname}; path=/;`;
|
||||
}
|
||||
window.location.reload();
|
||||
}}
|
||||
className={`border-2 p-3 rounded-lg text-center cursor-pointer transition-colors ${isActive ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-slate-200 dark:border-dark-border bg-white dark:bg-dark-card hover:bg-slate-50 dark:hover:bg-dark-surface'}`}
|
||||
>
|
||||
<div className="text-2xl mb-1">{lang.flag}</div>
|
||||
<div className="text-xs font-bold text-slate-700 dark:text-slate-300">{lang.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">A tradução é efetuada automaticamente e afetará toda a aplicação após recarregar a página.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 mb-3">Aparência</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
@@ -2426,7 +2518,7 @@
|
||||
<InputGroup label="Data" type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
|
||||
<InputGroup label="Horário" name="time" value={formData.time || ''} onChange={handleInputChange} placeholder="Ex: 14:00 - 16:00" required />
|
||||
</div>
|
||||
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="Nome do residente" required />
|
||||
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="Nome do residente" required disabled={userRole !== 'admin'} />
|
||||
|
||||
<div className="bg-slate-50 dark:bg-dark-card p-4 rounded-lg border border-slate-200 dark:border-dark-border mt-2 mb-4 flex justify-between items-center transition-colors">
|
||||
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">Custo Estimado:</span>
|
||||
@@ -2498,6 +2590,7 @@
|
||||
</ErrorBoundary>
|
||||
);
|
||||
</script>
|
||||
<div id="google_translate_element"></div>
|
||||
<!-- Firebase configs moved to top in React Module -->
|
||||
</body>
|
||||
</html>
|
||||
2
sw.js
2
sw.js
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'mycondominium-v3';
|
||||
const CACHE_NAME = 'mycondominium-v4';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'./',
|
||||
'./index.html',
|
||||
|
||||
396
temp_script.jsx
396
temp_script.jsx
@@ -1,89 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<title>MyCondominium</title>
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
bg: '#0f172a',
|
||||
surface: '#1e293b',
|
||||
card: '#334155',
|
||||
border: '#475569',
|
||||
text: '#f1f5f9',
|
||||
mute: '#94a3b8'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18.2.0",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
||||
"lucide-react": "https://esm.sh/lucide-react@0.292.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
@@ -93,10 +7,41 @@
|
||||
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
|
||||
MessageCircle, Paperclip, Send
|
||||
} from 'lucide-react';
|
||||
|
||||
import { app } from './firebase.js';
|
||||
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js';
|
||||
import { getDatabase, ref, push, set, onValue, remove, update } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
componentDidCatch(error, errorInfo) {
|
||||
this.setState({ error, errorInfo });
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ padding: '20px', backgroundColor: '#fee2e2', color: '#991b1b', fontFamily: 'sans-serif', height: '100vh' }}>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Algo correu mal (Erro na Aplicação)</h1>
|
||||
<pre style={{ marginTop: '20px', whiteSpace: 'pre-wrap', backgroundColor: '#fef2f2', padding: '15px', border: '1px solid #f87171' }}>
|
||||
{this.state.error && this.state.error.toString()}
|
||||
<br />
|
||||
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
<button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>Recarregar Página</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const auth = getAuth(app);
|
||||
const db = getDatabase(app);
|
||||
|
||||
@@ -303,12 +248,23 @@
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (isLoading) return;
|
||||
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
const success = await onLogin(email, password);
|
||||
|
||||
setIsLoading(false);
|
||||
if (!success) {
|
||||
setError('Email ou Palavra-passe incorreta');
|
||||
setTimeout(() => {
|
||||
setError('');
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -354,8 +310,15 @@
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
||||
Entrar
|
||||
<button type="submit" disabled={isLoading} className={`w-full ${isLoading ? 'bg-blue-400 dark:bg-blue-600/50 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/30'} text-white font-bold py-3 rounded-lg transition-colors flex justify-center items-center gap-2`}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
A entrar...
|
||||
</>
|
||||
) : (
|
||||
'Entrar'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -434,7 +397,7 @@
|
||||
userId = 'admin_001';
|
||||
} else {
|
||||
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
||||
if (residentUser && (password === residentUser.contact || password === '1234')) {
|
||||
if (residentUser && (password === residentUser.password || (!residentUser.password && password === residentUser.contact) || password === '1234')) {
|
||||
role = residentUser.role || 'morador';
|
||||
userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
|
||||
userId = residentUser.id || userId;
|
||||
@@ -494,7 +457,17 @@
|
||||
return onValue(ref(db, path), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (data) {
|
||||
let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
|
||||
let parsed = Object.entries(data).map(([id, val]) => {
|
||||
if (path === 'faturas' && val.status === 'Em Validação') {
|
||||
return { id, ...val, status: 'Pago' };
|
||||
}
|
||||
return { id, ...val };
|
||||
});
|
||||
|
||||
if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
|
||||
parsed = parsed.filter(item => item.moradorId === currentUserId);
|
||||
}
|
||||
|
||||
if (sortFunc) parsed = parsed.sort(sortFunc);
|
||||
setter(parsed);
|
||||
} else {
|
||||
@@ -669,8 +642,12 @@
|
||||
} else if (type === 'emitir_fatura') {
|
||||
setFormData(initialFaturaForm);
|
||||
} else if (type === 'booking') {
|
||||
const baseForm = initialBookingForm;
|
||||
const baseForm = { ...initialBookingForm };
|
||||
if (defaultFacility) baseForm.facility = defaultFacility;
|
||||
|
||||
// Preenche sempre o nome do utilizador logado por defeito
|
||||
baseForm.resident = currentUserName;
|
||||
|
||||
setFormData(baseForm);
|
||||
}
|
||||
};
|
||||
@@ -715,7 +692,8 @@
|
||||
try {
|
||||
if (editingItem) {
|
||||
const residentRef = ref(db, `condominos/${editingItem.id}`);
|
||||
await set(residentRef, {
|
||||
const updatedData = {
|
||||
...editingItem,
|
||||
unit: formData.unit || '',
|
||||
name: formData.name || '',
|
||||
contact: formData.contact || '',
|
||||
@@ -723,7 +701,11 @@
|
||||
status: formData.status || 'Pago',
|
||||
pending: Number(formData.pending) || 0,
|
||||
role: formData.role || 'morador'
|
||||
});
|
||||
};
|
||||
if (formData.password) {
|
||||
updatedData.password = formData.password;
|
||||
}
|
||||
await set(residentRef, updatedData);
|
||||
showNotification(`Condómino ${formData.name} atualizado`);
|
||||
} else {
|
||||
const residentsListRef = ref(db, 'condominos');
|
||||
@@ -733,6 +715,7 @@
|
||||
name: formData.name || '',
|
||||
contact: formData.contact || '',
|
||||
email: formData.email || '',
|
||||
password: formData.password || '1234',
|
||||
status: formData.status || 'Pago',
|
||||
pending: Number(formData.pending) || 0,
|
||||
role: formData.role || 'morador'
|
||||
@@ -792,7 +775,7 @@
|
||||
}
|
||||
try {
|
||||
const newIssueRef = push(ref(db, 'manutencao'));
|
||||
await set(newIssueRef, { ...formData });
|
||||
await set(newIssueRef, { ...formData, moradorId: currentUserId });
|
||||
|
||||
sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
|
||||
if (userRole !== 'admin') {
|
||||
@@ -846,10 +829,17 @@
|
||||
|
||||
const handlePayFatura = async (fatura) => {
|
||||
try {
|
||||
await set(ref(db, `faturas/${fatura.id}/status`), 'Em Validação');
|
||||
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi submetido. Aguarda validação.`, 'info', fatura.moradorId);
|
||||
sendSystemNotification(`Comprovativo recebido da fração ${fatura.fracao}.`, 'info', 'admin');
|
||||
showNotification("Comprovativo enviado! A aguardar validação do administrador.", "success");
|
||||
await set(ref(db, `faturas/${fatura.id}/status`), 'Pago');
|
||||
|
||||
const morador = residents.find(r => r.id === fatura.moradorId);
|
||||
if (morador) {
|
||||
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
|
||||
if (newPending < 0) newPending = 0;
|
||||
await set(ref(db, `condominos/${morador.id}/pending`), newPending);
|
||||
}
|
||||
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi concluído!`, 'success', fatura.moradorId);
|
||||
sendSystemNotification(`Pagamento registado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin');
|
||||
showNotification("Pagamento efetuado com sucesso!", "success");
|
||||
} catch (error) {
|
||||
console.error("Erro ao pagar fatura:", error);
|
||||
showNotification("Erro ao processar pagamento.", "error");
|
||||
@@ -900,7 +890,8 @@
|
||||
const bookingData = {
|
||||
...formData,
|
||||
facilityName: facilityNames[formData.facility],
|
||||
status: 'Confirmado'
|
||||
status: 'Confirmado',
|
||||
moradorId: currentUserId
|
||||
};
|
||||
|
||||
const newBookingRef = push(ref(db, 'reservas'));
|
||||
@@ -1210,6 +1201,110 @@
|
||||
const ProfileView = ({ theme, setTheme }) => {
|
||||
const [activeSection, setActiveSection] = useState('personal');
|
||||
|
||||
const isMorador = userRole !== 'admin';
|
||||
const [formData, setFormData] = useState({
|
||||
name: 'A carregar...',
|
||||
role: '...',
|
||||
email: '',
|
||||
contact: '',
|
||||
address: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isMorador) {
|
||||
const currentUserData = residents.find(r => r.id === currentUserId) || {};
|
||||
setFormData({
|
||||
name: currentUserData.name || currentUserName || '',
|
||||
role: `Fração ${currentUserData.unit || 'N/A'}`,
|
||||
email: currentUserData.email || '',
|
||||
contact: currentUserData.contact || '',
|
||||
address: 'Morada do Condomínio'
|
||||
});
|
||||
} else {
|
||||
const adminRef = ref(db, 'configuracoes/admin_profile');
|
||||
const unsub = onValue(adminRef, (snapshot) => {
|
||||
if (snapshot.exists()) {
|
||||
setFormData(snapshot.val());
|
||||
} else {
|
||||
setFormData({
|
||||
name: 'Administrador do Condomínio',
|
||||
role: 'Síndico / Gestor',
|
||||
email: 'admin@mycondominium.pt',
|
||||
contact: '+351 912 345 678',
|
||||
address: 'Rua das Flores, nº 123, Escritório 2B'
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => unsub();
|
||||
}
|
||||
}, [residents, currentUserId, userRole, currentUserName, isMorador]);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' });
|
||||
const handlePasswordChange = (field, value) => {
|
||||
setPasswordData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSavePassword = async () => {
|
||||
if (passwordData.new !== passwordData.confirm) {
|
||||
showNotification('As novas palavras-passe não coincidem.', 'error');
|
||||
return;
|
||||
}
|
||||
if (passwordData.new.length < 4) {
|
||||
showNotification('A nova palavra-passe deve ter pelo menos 4 caracteres.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMorador) {
|
||||
const currentUserData = residents.find(r => r.id === currentUserId);
|
||||
const currentPassword = currentUserData.password || currentUserData.contact || '1234';
|
||||
if (passwordData.current !== currentPassword) {
|
||||
showNotification('A palavra-passe atual está incorreta.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new);
|
||||
showNotification('Palavra-passe alterada com sucesso!', 'success');
|
||||
setPasswordData({ current: '', new: '', confirm: '' });
|
||||
sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin');
|
||||
} catch (error) {
|
||||
console.error("Erro ao alterar palavra-passe:", error);
|
||||
showNotification('Erro ao alterar a palavra-passe.', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification('A conta de administrador usa o Firebase Auth para gerir passwords.', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isMorador) {
|
||||
const currentUserData = residents.find(r => r.id === currentUserId);
|
||||
if (currentUserData && currentUserData.id) {
|
||||
try {
|
||||
await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email);
|
||||
await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact);
|
||||
showNotification('Dados atualizados com sucesso!', 'success');
|
||||
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
|
||||
} catch (error) {
|
||||
console.error("Erro ao guardar os dados:", error);
|
||||
showNotification('Erro ao guardar os dados.', 'error');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await set(ref(db, 'configuracoes/admin_profile'), formData);
|
||||
showNotification('Alterações guardadas com sucesso!', 'success');
|
||||
} catch (error) {
|
||||
console.error("Erro ao guardar perfil admin:", error);
|
||||
showNotification('Erro ao guardar as alterações.', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
|
||||
<div className="flex flex-col md:flex-row h-full">
|
||||
@@ -1258,19 +1353,16 @@
|
||||
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">Dados Pessoais</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InputGroup label="Nome Completo" value="Administrador do Condomínio" disabled />
|
||||
<InputGroup label="Cargo" value="Síndico / Gestor" disabled />
|
||||
<InputGroup label="Nome Completo" value={formData.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} />
|
||||
<InputGroup label={isMorador ? "Fração" : "Cargo"} value={formData.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InputGroup label="Email" value="admin@mycondominium.pt" type="email" />
|
||||
<InputGroup label="Telefone" value="+351 912 345 678" />
|
||||
<InputGroup label="Email" value={formData.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
|
||||
<InputGroup label="Telefone" value={formData.contact} onChange={(e) => handleChange('contact', e.target.value)} />
|
||||
</div>
|
||||
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
|
||||
<InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={formData.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} />
|
||||
<div className="flex justify-end mt-6">
|
||||
<button onClick={() => {
|
||||
showNotification('Alterações guardadas com sucesso!', 'success');
|
||||
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
|
||||
}} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
<button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
Guardar Alterações
|
||||
</button>
|
||||
</div>
|
||||
@@ -1294,16 +1386,13 @@
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" />
|
||||
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" value={passwordData.current} onChange={(e) => handlePasswordChange('current', e.target.value)} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InputGroup label="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" />
|
||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
|
||||
<InputGroup label="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" value={passwordData.new} onChange={(e) => handlePasswordChange('new', e.target.value)} />
|
||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" value={passwordData.confirm} onChange={(e) => handlePasswordChange('confirm', e.target.value)} />
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button onClick={() => {
|
||||
showNotification('Segurança atualizada com sucesso!', 'success');
|
||||
sendSystemNotification('Um utilizador alterou a palavra-passe.', 'info', 'admin');
|
||||
}} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
<button onClick={handleSavePassword} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
Atualizar Segurança
|
||||
</button>
|
||||
</div>
|
||||
@@ -1360,6 +1449,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">Idioma da Aplicação</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ code: 'pt', label: 'Português', flag: '🇵🇹' },
|
||||
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
||||
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr', label: 'Français', flag: '🇫🇷' }
|
||||
].map(lang => {
|
||||
const match = document.cookie.match(/googtrans=\/pt\/([a-z]{2})/);
|
||||
const activeLang = match ? match[1] : 'pt';
|
||||
const isActive = activeLang === lang.code;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lang.code}
|
||||
onClick={() => {
|
||||
if(lang.code === 'pt') {
|
||||
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + window.location.hostname + "; path=/;";
|
||||
} else {
|
||||
document.cookie = `googtrans=/pt/${lang.code}; path=/;`;
|
||||
document.cookie = `googtrans=/pt/${lang.code}; domain=${window.location.hostname}; path=/;`;
|
||||
}
|
||||
window.location.reload();
|
||||
}}
|
||||
className={`border-2 p-3 rounded-lg text-center cursor-pointer transition-colors ${isActive ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-slate-200 dark:border-dark-border bg-white dark:bg-dark-card hover:bg-slate-50 dark:hover:bg-dark-surface'}`}
|
||||
>
|
||||
<div className="text-2xl mb-1">{lang.flag}</div>
|
||||
<div className="text-xs font-bold text-slate-700 dark:text-slate-300">{lang.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">A tradução é efetuada automaticamente e afetará toda a aplicação após recarregar a página.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 mb-3">Aparência</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
@@ -1894,10 +2020,6 @@
|
||||
>
|
||||
Pagar
|
||||
</button>
|
||||
) : fatura.status === 'Em Validação' ? (
|
||||
<span className="text-orange-500 text-xs font-bold flex items-center justify-center gap-1">
|
||||
<Clock size={14} /> Em Validação
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-400 text-xs font-bold flex items-center justify-center gap-1">
|
||||
<CheckCircle size={14} /> Pago
|
||||
@@ -1950,6 +2072,25 @@
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Diário Financeiro</h3>
|
||||
<span className="text-xs bg-slate-100 dark:bg-dark-bg text-slate-600 dark:text-slate-400 px-2 py-1 rounded-full">{finances.length} movimentos</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{finances.length === 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
for (const item of INITIAL_FINANCES) {
|
||||
await set(push(ref(db, 'financas')), item);
|
||||
}
|
||||
showNotification("Dados de exemplo restaurados com sucesso!", "success");
|
||||
} catch (error) {
|
||||
console.error("Erro ao restaurar:", error);
|
||||
showNotification("Erro ao restaurar.", "error");
|
||||
}
|
||||
}}
|
||||
className="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors shadow-sm flex items-center gap-2"
|
||||
>
|
||||
<Wrench size={16} /> Restaurar Base de Dados
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleOpenModal('finance')}
|
||||
className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
|
||||
@@ -1957,6 +2098,7 @@
|
||||
<Plus size={18} /> Novo Registo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-auto flex-1">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border sticky top-0 z-10">
|
||||
@@ -2184,7 +2326,10 @@
|
||||
<InputGroup label="Fração" name="unit" value={formData.unit || ''} onChange={handleInputChange} placeholder="Ex: 1º Esq" required />
|
||||
<InputGroup label="Nome Completo" name="name" value={formData.name || ''} onChange={handleInputChange} placeholder="Nome do proprietário" required />
|
||||
<InputGroup label="Email" type="email" name="email" value={formData.email || ''} onChange={handleInputChange} placeholder="email@exemplo.com" />
|
||||
<InputGroup label="Contacto" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputGroup label="Telemóvel" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" required />
|
||||
<InputGroup label="Palavra-passe" type="text" name="password" value={formData.password || ''} onChange={handleInputChange} placeholder={editingItem ? "Deixar em branco para manter" : "1234"} required={!editingItem} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputGroup label="Estado" name="status" value={formData.status || 'Pago'} onChange={handleInputChange} options={[{ value: 'Pago', label: 'Pago' }, { value: 'Pendente', label: 'Pendente' }, { value: 'Atrasado', label: 'Atrasado' }]} />
|
||||
<InputGroup label="Valor Pendente (€)" type="number" name="pending" value={formData.pending || 0} onChange={handleInputChange} />
|
||||
@@ -2263,7 +2408,7 @@
|
||||
<InputGroup label="Data" type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
|
||||
<InputGroup label="Horário" name="time" value={formData.time || ''} onChange={handleInputChange} placeholder="Ex: 14:00 - 16:00" required />
|
||||
</div>
|
||||
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="Nome do residente" required />
|
||||
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="Nome do residente" required disabled={userRole !== 'admin'} />
|
||||
|
||||
<div className="bg-slate-50 dark:bg-dark-card p-4 rounded-lg border border-slate-200 dark:border-dark-border mt-2 mb-4 flex justify-between items-center transition-colors">
|
||||
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">Custo Estimado:</span>
|
||||
@@ -2329,7 +2474,8 @@
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
root.render(<App />);
|
||||
<!-- Firebase configs moved to top in React Module -->
|
||||
</body>
|
||||
</html>
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user