diff --git a/sigap-mobile/assets/images/animations/empty-address.json b/sigap-mobile/assets/images/animations/empty-address.json new file mode 100644 index 0000000..c8960a2 --- /dev/null +++ b/sigap-mobile/assets/images/animations/empty-address.json @@ -0,0 +1,1048 @@ +{ + "v": "5.9.4", + "fr": 29.9700012207031, + "ip": 0, + "op": 60.0000024438501, + "w": 390, + "h": 500, + "nm": "Icons", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Char Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 0, + "s": [0] + }, + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 6, + "s": [-9] + }, + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 17, + "s": [8] + }, + { + "i": { "x": [0.833], "y": [1] }, + "o": { "x": [0.167], "y": [0] }, + "t": 27, + "s": [-6] + }, + { + "i": { "x": [0.833], "y": [1] }, + "o": { "x": [0.167], "y": [0] }, + "t": 35, + "s": [4] + }, + { + "i": { "x": [0.833], "y": [1] }, + "o": { "x": [0.167], "y": [0] }, + "t": 41, + "s": [-3] + }, + { + "i": { "x": [0.833], "y": [1] }, + "o": { "x": [0.167], "y": [0] }, + "t": 45, + "s": [2] + }, + { + "i": { "x": [0.833], "y": [1] }, + "o": { "x": [0.167], "y": [0] }, + "t": 49, + "s": [-1] + }, + { "t": 52.0000021180034, "s": [0] } + ], + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [202.25, 387, 0], + "to": [0, 2, 0], + "ti": [0, -1.333, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 4, + "s": [202.25, 399, 0], + "to": [0, 1.333, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 8, + "s": [202.25, 395, 0], + "to": [0, 0, 0], + "ti": [0, -0.667, 0] + }, + { "t": 11.0000004480392, "s": [202.25, 399, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [202.25, 397.75, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [-195, 250], + [195, 250], + [195, -250], + [-195, -250] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [-195, -250], + [195, -250], + [195, 250], + [-195, 250] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 2, + "ty": "sh", + "ix": 3, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 1.536], + [7.504, -2.056], + [6.566, -3.821], + [5.462, -5.517], + [3.848, -6.79], + [1.929, -7.474], + [0, -7.503], + [-0.015, -0.55], + [-2.162, -8.129], + [-3.634, -8.808], + [-5.585, -10.448], + [-6.005, -9.399], + [-5.783, -7.527], + [-0.04, -0.051], + [-8.868, 0], + [-7.394, 9.584], + [-0.039, 0.052], + [-6.294, 9.853], + [-5.586, 10.446], + [-4.313, 10.456], + [-2.162, 8.13], + [-0.181, 6.835], + [0, 0.54], + [1.793, 6.949], + [3.723, 6.57], + [5.461, 5.516], + [6.769, 3.94], + [7.505, 2.055], + [8.165, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-4.396, -1.206], + [-3.852, -2.226], + [-3.305, -3.369], + [-2.267, -4.169], + [-1.047, -4.339], + [0, -4.086], + [0.019, -0.553], + [1.276, -4.176], + [2.256, -3.654], + [3.271, -3.099], + [4.013, -2.166], + [4.179, -1.07], + [4.518, 0], + [4.179, 1.07], + [3.702, 1.999], + [3.27, 3.099], + [2.372, 3.844], + [1.276, 4.175], + [0.148, 4.555], + [0, 0.558], + [-0.919, 3.808], + [-2.085, 3.835], + [-3.306, 3.369], + [-4.132, 2.387], + [-4.397, 1.206], + [-4.789, 0], + [-0.019, 0.769], + [0, 0.769], + [0.003, 1.844], + [0, 1.844], + [0, 0], + [0, 0], + [0.016, 1.535] + ], + "o": [ + [-8.166, 0], + [-7.505, 2.055], + [-6.77, 3.94], + [-5.462, 5.516], + [-3.724, 6.57], + [-1.793, 6.949], + [0, 0.54], + [0.182, 6.835], + [2.161, 8.13], + [4.312, 10.456], + [5.586, 10.446], + [6.295, 9.853], + [0.039, 0.052], + [7.395, 9.584], + [8.867, 0], + [0.04, -0.051], + [5.783, -7.527], + [6.005, -9.399], + [5.586, -10.448], + [3.632, -8.808], + [2.161, -8.129], + [0.015, -0.55], + [0, -7.503], + [-1.929, -7.474], + [-3.848, -6.79], + [-5.463, -5.517], + [-6.567, -3.821], + [-7.504, -2.056], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [4.789, 0], + [4.397, 1.206], + [4.131, 2.387], + [3.306, 3.369], + [2.085, 3.835], + [0.92, 3.808], + [0, 0.558], + [-0.148, 4.555], + [-1.275, 4.175], + [-2.372, 3.844], + [-3.269, 3.099], + [-3.702, 1.999], + [-4.18, 1.07], + [-4.519, 0], + [-4.179, -1.07], + [-4.013, -2.166], + [-3.27, -3.099], + [-2.256, -3.654], + [-1.276, -4.176], + [-0.018, -0.553], + [0, -4.086], + [1.048, -4.339], + [2.267, -4.169], + [3.305, -3.369], + [3.853, -2.226], + [4.396, -1.206], + [0, -0.769], + [0.017, -0.77], + [0, -1.845], + [-0.002, -1.844], + [0, 0], + [0, 0], + [0, -1.535], + [-0.015, -1.536] + ], + "v": [ + [6.989, -94.954], + [-16.586, -91.798], + [-37.762, -82.911], + [-56.188, -68.65], + [-70.23, -50.114], + [-78.782, -28.979], + [-81.53, -7.247], + [-81.482, -5.593], + [-77.839, 17.007], + [-69.02, 42.567], + [-53.959, 74.182], + [-36.359, 104.21], + [-17.994, 130.581], + [-17.875, 130.735], + [6.989, 147.68], + [31.852, 130.735], + [31.971, 130.581], + [50.336, 104.21], + [67.936, 74.182], + [82.998, 42.567], + [91.817, 17.007], + [95.458, -5.593], + [95.507, -7.247], + [92.759, -28.979], + [84.208, -50.114], + [70.166, -68.65], + [51.74, -82.911], + [30.563, -91.798], + [6.989, -94.954], + [6.989, -90.347], + [6.989, -85.74], + [6.989, -79.957], + [6.989, -74.173], + [6.989, -68.639], + [6.989, -63.105], + [6.989, -60.799], + [6.989, -58.492], + [20.804, -56.637], + [33.216, -51.442], + [44.424, -42.763], + [52.836, -31.411], + [57.576, -19.113], + [58.983, -7.247], + [58.898, -5.594], + [56.725, 7.537], + [51.39, 19.316], + [42.888, 29.777], + [31.925, 37.722], + [20.07, 42.359], + [6.989, 43.998], + [-6.092, 42.359], + [-17.948, 37.722], + [-28.911, 29.777], + [-37.413, 19.316], + [-42.748, 7.537], + [-44.921, -5.594], + [-45.006, -7.247], + [-43.599, -19.113], + [-38.859, -31.411], + [-30.447, -42.763], + [-19.239, -51.442], + [-6.827, -56.637], + [6.989, -58.492], + [7.025, -60.799], + [7.06, -63.107], + [7.055, -68.64], + [7.05, -74.172], + [7.05, -79.957], + [7.05, -85.741], + [7.019, -90.347] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 4", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "mm", + "mm": 1, + "nm": "Merge Paths 1", + "mn": "ADBE Vector Filter - Merge", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.2196, 0.6314, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [195, 250], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 5, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Shadow Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [201, 398, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [201, 398, 0], "ix": 1, "l": 2 }, + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0] }, + "t": 0, + "s": [90, 90, 100] + }, + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0] }, + "t": 4, + "s": [100, 100, 100] + }, + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0] }, + "t": 8, + "s": [97, 97, 100] + }, + { "t": 11.0000004480392, "s": [100, 100, 100] } + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -8.122], + [35.637, 0], + [0, 8.122], + [-35.638, 0] + ], + "o": [ + [0, 8.122], + [-35.638, 0], + [0, -8.122], + [35.637, 0] + ], + "v": [ + [64.527, 0], + [0, 14.706], + [-64.527, 0], + [0, -14.706] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.85, 0.85, 0.85, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.989, 397.68], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 4, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "House Outlines", + "parent": 1, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0.104, "ix": 10 }, + "p": { "a": 0, "k": [195.27, 248.737, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [195, 250, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, -1.663], + [0, 0], + [1.207, 0], + [0, 0], + [0, 1.599], + [0, 0], + [-1.631, 0] + ], + "o": [ + [1.207, 0], + [0, 0], + [0, 1.599], + [0, 0], + [-1.631, 0], + [0, 0], + [0, -1.663], + [0, 0] + ], + "v": [ + [18.645, 0.212], + [21.484, 3.083], + [21.484, 14.535], + [18.645, 17.374], + [8.793, 17.374], + [5.921, 14.535], + [5.921, 3.083], + [8.793, 0.212] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [34.991, 7.585], + [46.476, 7.585], + [0.211, -44.778], + [-46.476, 7.585], + [-35.024, 7.585], + [-35.024, 46.442], + [34.991, 46.442] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 2, + "ty": "sh", + "ix": 3, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [29.673, -46.442], + [29.673, -45.596], + [29.673, -17.798], + [28.433, -17.373], + [5.138, -45.202], + [3.866, -46.442], + [5.53, -46.442], + [28.857, -46.442] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 3", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "mm", + "mm": 1, + "nm": "Merge Paths 1", + "mn": "ADBE Vector Filter - Merge", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.85, 0.85, 0.85, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.989, 250.589], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 7, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Cloud1 Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [195, 250, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [195, 250, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [10.361, 0], + [0, 0], + [0, 10.361], + [-10.361, 0], + [0, 0], + [0, -10.361] + ], + "o": [ + [0, 0], + [-10.361, 0], + [0, -10.361], + [0, 0], + [10.361, 0], + [0, 10.361] + ], + "v": [ + [120.003, 18.838], + [-120.003, 18.838], + [-138.841, 0], + [-120.003, -18.838], + [120.003, -18.838], + [138.841, 0] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.96, 0.96, 0.96, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.989, 189.706], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 4, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Cloud2 Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [195, 250, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [195, 250, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [10.361, 0], + [0, 0], + [0, 10.361], + [-10.36, 0], + [0, 0], + [0, -10.361] + ], + "o": [ + [0, 0], + [-10.36, 0], + [0, -10.361], + [0, 0], + [10.361, 0], + [0, 10.361] + ], + "v": [ + [145.329, 18.838], + [-145.329, 18.838], + [-164.167, 0], + [-145.329, -18.838], + [145.329, -18.838], + [164.167, 0] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.96, 0.96, 0.96, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.988, 223.431], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 4, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Cloud3 Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [195, 250, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [195, 250, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [10.361, 0], + [0, 0], + [0, 10.361], + [-10.361, 0], + [0, 0], + [0, -10.361] + ], + "o": [ + [0, 0], + [-10.361, 0], + [0, -10.361], + [0, 0], + [10.361, 0], + [0, 10.361] + ], + "v": [ + [101.004, 18.838], + [-101.004, 18.838], + [-119.842, 0], + [-101.004, -18.838], + [101.004, -18.838], + [119.842, 0] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.96, 0.96, 0.96, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.989, 284.924], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 4, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60.0000024438501, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [] +} diff --git a/sigap-mobile/lib/splash_screen.dart b/sigap-mobile/lib/splash_screen.dart index 84c4b1f..e7cacbb 100644 --- a/sigap-mobile/lib/splash_screen.dart +++ b/sigap-mobile/lib/splash_screen.dart @@ -13,22 +13,12 @@ class AnimatedSplashScreenWidget extends StatelessWidget { Widget build(BuildContext context) { final isDark = THelperFunctions.isDarkMode(context); - // Try to find SplashController, but don't fail if it's not ready yet - // SplashController? splashController; - // if (Get.isRegistered()) { - // splashController = Get.find(); - // } else { - // // Register a temporary controller if the real one isn't ready - // splashController = Get.put(SplashController()); - // } - return AnimatedSplashScreen( splash: Center( child: Lottie.asset( isDark ? TImages.darkSplashApp : TImages.lightSplashApp, frameRate: FrameRate.max, repeat: true, - ), ), splashIconSize: 300, @@ -38,30 +28,3 @@ class AnimatedSplashScreenWidget extends StatelessWidget { ); } } - -// A transition screen that shows a loading indicator -// until authentication is ready -// class _LoadingScreen extends StatelessWidget { -// const _LoadingScreen(); - -// @override -// Widget build(BuildContext context) { -// final isDark = THelperFunctions.isDarkMode(context); - -// // This will be shown after the animated splash screen -// // while we wait for initialization to complete -// return Scaffold( -// backgroundColor: isDark ? TColors.dark : TColors.white, -// body: const Center( -// child: Column( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// CircularProgressIndicator(), -// SizedBox(height: 24), -// Text("Menyiapkan aplikasi..."), -// ], -// ), -// ), -// ); -// } -// } diff --git a/sigap-mobile/lib/src/cores/routes/app_pages.dart b/sigap-mobile/lib/src/cores/routes/app_pages.dart index 32022b6..54f1405 100644 --- a/sigap-mobile/lib/src/cores/routes/app_pages.dart +++ b/sigap-mobile/lib/src/cores/routes/app_pages.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:sigap/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; @@ -38,6 +39,11 @@ class AppPages { name: AppRoutes.signupWithRole, page: () => const SignupWithRoleScreen(), ), + + GetPage( + name: AppRoutes.emailVerification, + page: () => const EmailVerificationScreen(), + ), GetPage( name: AppRoutes.forgotPassword, diff --git a/sigap-mobile/lib/src/cores/services/location_service.dart b/sigap-mobile/lib/src/cores/services/location_service.dart index 9a313bb..0e2c22e 100644 --- a/sigap-mobile/lib/src/cores/services/location_service.dart +++ b/sigap-mobile/lib/src/cores/services/location_service.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; +import 'package:logger/logger.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; class LocationService extends GetxService { @@ -143,6 +144,7 @@ class LocationService extends GetxService { // Get city name from coordinates if (currentPosition.value != null) { + await _updateCityName(); } @@ -203,6 +205,8 @@ class LocationService extends GetxService { if (placemarks.isNotEmpty) { currentCity.value = placemarks.first.locality ?? ''; } + + Logger().i('Current city: ${currentCity.value}'); } catch (e) { currentCity.value = ''; } diff --git a/sigap-mobile/lib/src/cores/services/supabase_service.dart b/sigap-mobile/lib/src/cores/services/supabase_service.dart index 474d123..9bcf199 100644 --- a/sigap-mobile/lib/src/cores/services/supabase_service.dart +++ b/sigap-mobile/lib/src/cores/services/supabase_service.dart @@ -26,7 +26,7 @@ class SupabaseService extends GetxService { bool get isAuthenticated => currentUser != null; /// Check if current user is an officer based on metadata - bool get isOfficer => userMetadata.isOfficer ?? false; + bool get isOfficer => userMetadata.isOfficer; /// Get the stored identifier (NIK or NRP) of the current user String? get userIdentifier { diff --git a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart index 83f5075..8f59813 100644 --- a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart +++ b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart @@ -8,7 +8,7 @@ class UserMetadataModel { // Core properties that define the user type final bool isOfficer; final String? userId; - final String? roleId; + final String? roleId; final String profileStatus; // Related models that hold specific data diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index 4145597..a90252d 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -1,12 +1,13 @@ +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:logger/logger.dart'; import 'package:sigap/src/cores/services/biometric_service.dart'; import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; -import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:sigap/src/utils/exceptions/format_exceptions.dart'; @@ -28,13 +29,17 @@ class AuthenticationRepository extends GetxController { // Getters that use the Supabase service User? get authUser => SupabaseService.instance.currentUser; String? get currentUserId => SupabaseService.instance.currentUserId; + Session? get currentSession => _supabase.auth.currentSession; // --------------------------------------------------------------------------- // LIFECYCLE & REDIRECT // --------------------------------------------------------------------------- @override void onReady() { - screenRedirect(); + // Delay the redirect to avoid issues during build + Future.delayed(Duration.zero, () { + screenRedirect(); + }); } // Check for biometric login on app start @@ -63,40 +68,54 @@ class AuthenticationRepository extends GetxController { } } - // Redirect user to appropriate screen on app start - screenRedirect() async { - final session = _supabase.auth.currentSession; + /// Updated screenRedirect method to accept arguments + void screenRedirect({UserMetadataModel? arguments}) async { + // Use addPostFrameCallback to ensure navigation happens after the build cycle + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + final session = _supabase.auth.currentSession; - // Check if onboarding has been shown before - final isFirstTime = storage.read('isFirstTime') ?? false; - - if (session != null) { - if (session.user.emailConfirmedAt == null) { - // User is not verified, go to email verification screen - Get.offAllNamed(AppRoutes.emailVerification); - } else if (session.user.userMetadata!['profile_status'] == 'incomplete') { - // User is regular user, go to main app screen - Get.offAllNamed(AppRoutes.registrationForm); - } else if (session.user.userMetadata!['profile_status'] == 'complete' && - session.user.emailConfirmedAt != null) { - // Redirect to the main app screen - Get.offAllNamed(AppRoutes.panicButton); - } - } else { - // Try biometric login first - bool biometricSuccess = await attemptBiometricLogin(); - if (!biometricSuccess) { - // If not first time, go to sign in directly - // If first time, show onboarding first - if (isFirstTime) { - Get.offAll(() => const SignInScreen()); - } else { - // Mark that onboarding has been shown - storage.write('isFirstTime', true); - Get.offAll(() => const OnboardingScreen()); + if (await _locationService.isLocationValidForFeature() == false) { + // Location is not valid, navigate to warning screen + Get.offAllNamed(AppRoutes.locationWarning); + return; } + + if (session != null) { + if (session.user.emailConfirmedAt == null) { + // User is not verified, go to email verification screen + Get.offAllNamed(AppRoutes.emailVerification); + } else if (session.user.userMetadata?['profile_status'] == + 'incomplete' && + session.user.emailConfirmedAt != null) { + // User is incomplete, go to registration form with arguments if provided + Get.offAllNamed(AppRoutes.registrationForm); + } else { + // User is logged in and verified, go to the main app screen + Get.offAllNamed(AppRoutes.panicButton); + } + } else { + // Try biometric login first - but only if we're not already in a navigation + if (Get.currentRoute != AppRoutes.signIn && + Get.currentRoute != AppRoutes.onboarding) { + bool biometricSuccess = await attemptBiometricLogin(); + if (!biometricSuccess) { + // If not first time, go to sign in directly + // If first time, show onboarding first + storage.writeIfNull('isFirstTime', true); + // check if user is already logged in + storage.read('isFirstTime') != true + ? Get.offAllNamed(AppRoutes.signIn) + : Get.offAllNamed(AppRoutes.onboarding); + } + } + } + } catch (e) { + Logger().e('Error in screenRedirect: $e'); + // Fallback to sign in screen on error + Get.offAll(() => const SignInScreen()); } - } + }); } // --------------------------------------------------------------------------- @@ -544,7 +563,7 @@ class AuthenticationRepository extends GetxController { initialData, email: email, ); - + final AuthResponse res = await _supabase.auth.signUp( email: email, password: password, @@ -567,12 +586,10 @@ class AuthenticationRepository extends GetxController { completeData, profileStatus: 'complete', ); - + // First update auth metadata await _supabase.auth.updateUser( - UserAttributes( - data: userMetadataModel.toProfileCompletionJson(), - ), + UserAttributes(data: userMetadataModel.toProfileCompletionJson()), ); // Then update or insert relevant tables based on the role @@ -616,7 +633,6 @@ class AuthenticationRepository extends GetxController { } } - // Update user role (officer/user) and metadata Future updateUserRole({ required bool isOfficer, @@ -651,7 +667,6 @@ class AuthenticationRepository extends GetxController { // Add these methods to the AuthenticationRepository class - // --------------------------------------------------------------------------- // BIOMETRIC AUTHENTICATION // --------------------------------------------------------------------------- diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/email_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/email_verification_controller.dart index 1d40438..eccd89d 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/email_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/email_verification_controller.dart @@ -7,6 +7,9 @@ import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; class EmailVerificationController extends GetxController { + // Singleton instance + static EmailVerificationController get instance => Get.find(); + // OTP text controllers final List otpControllers = List.generate( 6, diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/forgot_password_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/forgot_password_controller.dart index 021d4f6..6e73222 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/forgot_password_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/forgot_password_controller.dart @@ -5,6 +5,9 @@ import 'package:sigap/src/utils/popups/loaders.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class ForgotPasswordController extends GetxController { + // Singleton instance + static ForgotPasswordController get instance => Get.find(); + // Form key for validation final formKey = GlobalKey(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart index fe7eea2..1a77dd7 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart @@ -1,4 +1,6 @@ import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:logger/logger.dart'; import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart'; @@ -10,6 +12,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/steps/unit_info import 'package:sigap/src/features/daily-ops/data/models/index.dart'; import 'package:sigap/src/features/personalization/data/models/index.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; +import 'package:sigap/src/utils/constants/num_int.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; class FormRegistrationController extends GetxController { @@ -25,6 +28,8 @@ class FormRegistrationController extends GetxController { late final IdentityVerificationController identityController; late final OfficerInfoController? officerInfoController; late final UnitInfoController? unitInfoController; + + final storage = GetStorage(); // Current step index final RxInt currentStep = 0.obs; @@ -35,7 +40,7 @@ class FormRegistrationController extends GetxController { // User metadata model final Rx userMetadata = UserMetadataModel().obs; - // Vievewer data + // Viewer data final Rx viewerModel = Rx(null); final Rx profileModel = Rx(null); @@ -48,77 +53,207 @@ class FormRegistrationController extends GetxController { @override void onInit() { super.onInit(); + // Initialize user data directly from current session + _initializeFromCurrentUser(); + } - // Get role and initial data from arguments - final arguments = Get.arguments; - if (arguments != null) { - if (arguments['role'] != null) { - selectedRole.value = arguments['role'] as RoleModel; - } + /// Initialize the controller directly from current user session + void _initializeFromCurrentUser() async { + try { + Logger().d('Initializing registration form from current user'); - if (arguments['initialData'] != null) { - // Initialize with data from signup - userMetadata.value = arguments['initialData'] as UserMetadataModel; - } + // Get the current user session from AuthenticationRepository + final session = AuthenticationRepository.instance.currentSession; - // Store userId if provided - if (arguments['userId'] != null) { - userMetadata.value = userMetadata.value.copyWith( - userId: arguments['userId'], - ); - } - - // Initialize userMetadata with the selected role information - if ((userMetadata.value.roleId?.isEmpty ?? true) && - selectedRole.value != null) { - userMetadata.value = userMetadata.value.copyWith( - roleId: selectedRole.value!.id, - isOfficer: selectedRole.value!.isOfficer, - ); - } - - _initializeControllers(); - } else { - TLoaders.errorSnackBar( - title: 'Error', - message: 'No role selected. Please go back and select a role.', + // Initialize with default metadata + UserMetadataModel metadata = const UserMetadataModel( + profileStatus: 'incomplete', + isOfficer: false, ); - } - if (selectedRole.value?.isOfficer == true) { - _fetchAvailableUnits(); + // If there is an active session, use that data + if (session?.user != null) { + final user = session!.user; + Logger().d('Found active user session: ${user.id} - ${user.email}'); + + // Extract metadata from user session + metadata = UserMetadataModel( + userId: user.id, + email: user.email, + roleId: user.userMetadata?['role_id'] as String?, + isOfficer: user.userMetadata?['is_officer'] as bool? ?? false, + profileStatus: + user.userMetadata?['profile_status'] as String? ?? 'incomplete', + ); + + // If user has additional metadata and it's in the expected format, use it + if (user.userMetadata != null) { + try { + // Try to parse complete metadata if available + final fullMetadata = UserMetadataModel.fromJson(user.userMetadata); + metadata = fullMetadata; + Logger().d('Successfully parsed complete user metadata'); + } catch (e) { + Logger().w('Could not parse full metadata object: $e'); + // Continue with the basic metadata already created + } + } + } else { + // No active session, check if any arguments were passed + final arguments = Get.arguments; + + // If arguments contain a user ID, use it as fallback + if (arguments is Map && + arguments.containsKey('userId')) { + metadata = metadata.copyWith( + userId: arguments['userId'] as String?, + email: arguments['email'] as String?, + roleId: arguments['roleId'] as String?, + isOfficer: arguments['isOfficer'] as bool? ?? false, + ); + Logger().d('Using arguments as fallback: ${metadata.userId}'); + } else { + // No user data available, check temporary storage + final tempUserId = storage.read('TEMP_USER_ID') as String?; + final tempEmail = storage.read('CURRENT_USER_EMAIL') as String?; + + if (tempUserId != null || tempEmail != null) { + metadata = metadata.copyWith( + userId: tempUserId, + email: tempEmail, + roleId: storage.read('TEMP_ROLE_ID') as String?, + isOfficer: storage.read('TEMP_IS_OFFICER') as bool? ?? false, + ); + Logger().d( + 'Using temporary storage as fallback: ${metadata.userId}', + ); + } else { + Logger().w('No user data available, using default empty metadata'); + } + } + } + + // Set the user metadata + userMetadata.value = metadata; + Logger().d('Final user metadata: ${userMetadata.value.toString()}'); + + // Complete initialization + await _finalizeInitialization(); + } catch (e) { + Logger().e('Error initializing from current user: $e'); + userMetadata.value = const UserMetadataModel( + profileStatus: 'incomplete', + isOfficer: false, + ); + await _finalizeInitialization(); + } + } + + /// Finalize initialization after metadata is set + Future _finalizeInitialization() async { + try { + // Initialize form controllers + _initializeControllers(); + + // Set role information if available + if (userMetadata.value.roleId?.isNotEmpty == true) { + await _setRoleFromMetadata(); + } + + // Fetch units if user is an officer + if (userMetadata.value.isOfficer || + (selectedRole.value?.isOfficer == true)) { + await _fetchAvailableUnits(); + } + + Logger().d('Initialization completed successfully'); + } catch (e) { + Logger().e('Error in finalization: $e'); + } + } + + /// Set role information from metadata + Future _setRoleFromMetadata() async { + try { + final roleId = userMetadata.value.roleId; + if (roleId?.isNotEmpty == true) { + // Try to find the role in available roles + final role = await _findRoleById(roleId!); + if (role != null) { + selectedRole.value = role; + Logger().d('Role set from metadata: ${role.name}'); + } + } + } catch (e) { + Logger().e('Error setting role from metadata: $e'); + } + } + + /// Find role by ID (implement based on your role management system) + Future _findRoleById(String roleId) async { + try { + // Implement based on your role fetching logic + // This is a placeholder - replace with your actual implementation + return null; + } catch (e) { + Logger().e('Error finding role by ID: $e'); + return null; } } void _initializeControllers() { - final isOfficer = selectedRole.value?.isOfficer ?? false; + final isOfficer = userMetadata.value.isOfficer; - // Always initialize personal info controller - personalInfoController = Get.put(PersonalInfoController()); + // Initialize controllers with built-in static form keys + Get.put(PersonalInfoController(), permanent: false); - // Initialize ID Card verification controller - idCardVerificationController = Get.put( + Get.put( IdCardVerificationController(isOfficer: isOfficer), + permanent: false, ); - // Initialize Selfie verification controller - selfieVerificationController = Get.put(SelfieVerificationController()); + Get.put( + SelfieVerificationController(), + permanent: false, + ); - // Initialize identity verification controller - identityController = Get.put( + Get.put( IdentityVerificationController(isOfficer: isOfficer), + permanent: false, ); + // Initialize officer-specific controllers only if user is an officer if (isOfficer) { - // Initialize officer-specific controllers - officerInfoController = Get.put(OfficerInfoController()); - unitInfoController = Get.put(UnitInfoController()); - totalSteps = 5; // Personal, ID Card, Selfie, Officer Info, Unit Info + Get.put(OfficerInfoController(), permanent: false); + + Get.put(UnitInfoController(), permanent: false); + + totalSteps = + TNum.totalStepOfficer; // Personal, ID Card, Selfie, Officer Info, Unit Info + + // Assign officer-specific controllers + officerInfoController = Get.find(); + unitInfoController = Get.find(); } else { // For civilian users officerInfoController = null; unitInfoController = null; - totalSteps = 4; // Personal, ID Card, Selfie, Identity + totalSteps = TNum.totalStepViewer; // Personal, ID Card, Selfie, Identity + } + + // Assign shared controllers + personalInfoController = Get.find(); + idCardVerificationController = Get.find(); + selfieVerificationController = Get.find(); + identityController = Get.find(); + + // Initialize selectedRole based on isOfficer + if (selectedRole.value == null && + userMetadata.value.additionalData != null) { + final roleData = userMetadata.value.additionalData?['role']; + if (roleData != null) { + selectedRole.value = roleData as RoleModel; + } } } @@ -315,7 +450,6 @@ class FormRegistrationController extends GetxController { }, ); } else { - // Regular user - create profile-related data final viewerData = viewerModel.value?.copyWith( phone: personalInfoController.phoneController.text, diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin_controller.dart index 8f2de7b..e411937 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin_controller.dart @@ -7,6 +7,9 @@ import 'package:sigap/src/utils/helpers/network_manager.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; class SignInController extends GetxController { + // Singleton instance + static SignInController get instance => Get.find(); + final rememberMe = false.obs; final isPasswordVisible = false.obs; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_controller.dart index 22e95d6..387cd3c 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_controller.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:logger/logger.dart'; -import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; +import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/helpers/network_manager.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart index 9d74df4..0de9728 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart @@ -12,6 +12,7 @@ import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/helpers/network_manager.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; import 'package:sigap/src/utils/validators/validation.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; // Define the role types enum RoleType { viewer, officer } @@ -166,6 +167,7 @@ class SignupWithRoleController extends GetxController { } // Sign up function + /// Updated signup function with better error handling and argument passing void signUp(bool isOfficer) async { if (!validateSignupForm()) { return; @@ -184,21 +186,30 @@ class SignupWithRoleController extends GetxController { return; } - // Make sure we have a role selected + // Ensure we have a role selected if (selectedRoleId.value.isEmpty) { - // Find a role based on the selected role type _updateSelectedRoleBasedOnType(); } - // Create initial user metadata + // Validate role selection + if (selectedRoleId.value.isEmpty) { + TLoaders.errorSnackBar( + title: 'Role Required', + message: 'Please select a role before continuing.', + ); + return; + } + + // Create comprehensive initial user metadata final initialMetadata = UserMetadataModel( email: emailController.text.trim(), roleId: selectedRoleId.value, isOfficer: isOfficer, + profileStatus: 'incomplete', ); try { - // First create the basic account with email/password + // Create the account final authResponse = await AuthenticationRepository.instance .initialSignUp( email: emailController.text.trim(), @@ -206,48 +217,57 @@ class SignupWithRoleController extends GetxController { initialData: initialMetadata, ); - // Check if authResponse has a user property - if (authResponse.user == null || authResponse.session == null) { + // Validate response + if (authResponse.session == null || authResponse.user == null) { throw Exception('Failed to create account. Please try again.'); } - // Store email for verification - storage.write('CURRENT_USER_EMAIL', emailController.text.trim()); - storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken); - storage.write('TEMP_USER_ID', authResponse.user?.id); - storage.write('TEMP_ROLE_ID', selectedRoleId.value); + final user = authResponse.user!; + Logger().d('Account created successfully for user: ${user.id}'); + + // Store temporary data for verification process + await _storeTemporaryData(authResponse, isOfficer); + + // Navigate with arguments + AuthenticationRepository.instance.screenRedirect(); - // Navigate to registration form - Get.offNamed( - AppRoutes.registrationForm, - arguments: { - 'role': selectedRole.value, - 'userId': authResponse.user?.id, - 'initialData': initialMetadata, - }, - ); } catch (authError) { - // Handle specific authentication errors - Logger().e('Error during signup: $authError'); + Logger().e('Authentication error during signup: $authError'); TLoaders.errorSnackBar( title: 'Registration Failed', - message: authError.toString(), + message: _getReadableErrorMessage(authError.toString()), ); - // Important: Do not navigate or redirect on error return; } } catch (e) { - Logger().e('Error during signup: $e'); + Logger().e('Unexpected error during signup: $e'); TLoaders.errorSnackBar( title: 'Registration Failed', - message: e.toString(), + message: 'An unexpected error occurred. Please try again.', ); - // No navigation on error } finally { isLoading.value = false; } } + /// Store temporary data for the verification process + Future _storeTemporaryData( + AuthResponse authResponse, + bool isOfficer, + ) async { + try { + await storage.write('CURRENT_USER_EMAIL', emailController.text.trim()); + await storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken); + await storage.write('TEMP_USER_ID', authResponse.user?.id); + await storage.write('TEMP_ROLE_ID', selectedRoleId.value); + await storage.write('TEMP_IS_OFFICER', isOfficer); + + Logger().d('Temporary data stored successfully'); + } catch (e) { + Logger().e('Failed to store temporary data: $e'); + } + } + // Sign in with Google Future signInWithGoogle() async { try { @@ -278,7 +298,7 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; - + if (userId == null) { throw Exception("Failed to authenticate. Please try again."); } @@ -353,7 +373,7 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; - + if (userId == null) { throw Exception( "Failed to authenticate with Apple ID. Please try again.", @@ -423,7 +443,7 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; - + if (userId == null) { throw Exception( "Failed to authenticate with Facebook. Please try again.", @@ -496,7 +516,7 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; - + if (userId == null) { throw Exception( "Failed to sign in with email. Please check your credentials and try again.", @@ -517,7 +537,7 @@ class SignupWithRoleController extends GetxController { // Navigate to registration form to complete profile Get.offNamed( - AppRoutes.registrationForm, + AppRoutes.emailVerification, arguments: { 'role': selectedRole.value, 'userId': userId, @@ -545,4 +565,19 @@ class SignupWithRoleController extends GetxController { void goToSignIn() { Get.offNamed(AppRoutes.signIn); } + + /// Convert technical error messages to user-friendly messages + String _getReadableErrorMessage(String error) { + if (error.contains('email')) { + return 'Please check your email address and try again.'; + } else if (error.contains('password')) { + return 'Password must be at least 6 characters long.'; + } else if (error.contains('network') || error.contains('connection')) { + return 'Network error. Please check your connection and try again.'; + } else if (error.contains('already registered') || + error.contains('already exists')) { + return 'This email is already registered. Please try signing in instead.'; + } + return 'Registration failed. Please try again.'; + } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart index 08167db..ce58432 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart @@ -4,12 +4,20 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:sigap/src/cores/services/azure_ocr_service.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; class IdCardVerificationController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static IdCardVerificationController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.idCardVerification(); final AzureOCRService _ocrService = AzureOCRService(); final bool isOfficer; + // Maximum allowed file size in bytes (4MB) + final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes + IdCardVerificationController({required this.isOfficer}); // ID Card variables @@ -51,7 +59,7 @@ class IdCardVerificationController extends GetxController { idCardValidationMessage.value = ''; } - // Pick ID Card Image + // Pick ID Card Image with file size validation Future pickIdCardImage(ImageSource source) async { try { isUploadingIdCard.value = true; @@ -61,10 +69,21 @@ class IdCardVerificationController extends GetxController { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: source, - imageQuality: 80, + imageQuality: 80, // Reduce quality to help with file size ); if (image != null) { + // Check file size + final File file = File(image.path); + final int fileSize = await file.length(); + + if (fileSize > maxFileSizeBytes) { + idCardError.value = + 'Image size exceeds 4MB limit. Please choose a smaller image or lower resolution.'; + isIdCardValid.value = false; + return; + } + // Add artificial delay to show loading state await Future.delayed(const Duration(seconds: 1)); idCardImage.value = image; @@ -105,7 +124,7 @@ class IdCardVerificationController extends GetxController { idCardImage.value!, isOfficer, ); - + // If we get here without an exception, the image is likely valid isImageValid = result.isNotEmpty; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart index 1acf08c..62edc53 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart @@ -2,10 +2,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/cores/services/azure_ocr_service.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class IdentityVerificationController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static IdentityVerificationController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.identityVerification(); final AzureOCRService _ocrService = AzureOCRService(); final bool isOfficer; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart index acc2b9d..675ab3a 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart @@ -4,6 +4,9 @@ import 'package:image_picker/image_picker.dart'; import 'package:sigap/src/cores/services/azure_ocr_service.dart'; class ImageVerificationController extends GetxController { + // Singleton instance + static ImageVerificationController get instance => Get.find(); + final GlobalKey formKey = GlobalKey(); final AzureOCRService _ocrService = AzureOCRService(); final bool isOfficer; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart index f9aad61..06e833e 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class OfficerInfoController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static OfficerInfoController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.officerInfo(); // Controllers final nrpController = TextEditingController(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/personal_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/personal_info_controller.dart index 1ea2b1e..692f4bb 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/personal_info_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/personal_info_controller.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class PersonalInfoController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static PersonalInfoController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.personalInfo(); // Controllers final firstNameController = TextEditingController(); @@ -21,6 +26,7 @@ class PersonalInfoController extends GetxController { final RxString bioError = ''.obs; final RxString addressError = ''.obs; + @override void onInit() { super.onInit(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/selfie_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/selfie_verification_controller.dart index 3ce9d23..05ad01c 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/selfie_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/selfie_verification_controller.dart @@ -1,12 +1,22 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:sigap/src/cores/services/azure_ocr_service.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; class SelfieVerificationController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static SelfieVerificationController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.selfieVerification(); final AzureOCRService _ocrService = AzureOCRService(); + // Maximum allowed file size in bytes (4MB) + final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes + // Face verification variables final Rx selfieImage = Rx(null); final RxString selfieError = RxString(''); @@ -51,7 +61,7 @@ class SelfieVerificationController extends GetxController { selfieValidationMessage.value = ''; } - // Take or pick selfie image + // Take or pick selfie image with file size validation Future pickSelfieImage(ImageSource source) async { try { isUploadingSelfie.value = true; @@ -62,10 +72,21 @@ class SelfieVerificationController extends GetxController { final XFile? image = await picker.pickImage( source: source, preferredCameraDevice: CameraDevice.front, - imageQuality: 80, + imageQuality: 80, // Reduce quality to help with file size ); if (image != null) { + // Check file size + final File file = File(image.path); + final int fileSize = await file.length(); + + if (fileSize > maxFileSizeBytes) { + selfieError.value = + 'Image size exceeds 4MB limit. Please take a lower resolution photo.'; + isSelfieValid.value = false; + return; + } + // Add artificial delay to show loading state await Future.delayed(const Duration(seconds: 1)); selfieImage.value = image; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart index 748b998..50c79e4 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/daily-ops/data/models/index.dart'; +import 'package:sigap/src/utils/constants/form_key.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class UnitInfoController extends GetxController { - final GlobalKey formKey = GlobalKey(); + // Singleton instance + static UnitInfoController get instance => Get.find(); + + // Static form key + final GlobalKey formKey = TGlobalFormKey.unitInfo(); // Controllers final positionController = TextEditingController(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart index 0ed7af8..baf8d92 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart @@ -34,15 +34,14 @@ class FormRegistrationScreen extends StatelessWidget { return Scaffold( backgroundColor: dark ? TColors.dark : TColors.light, appBar: AppBar( + automaticallyImplyLeading: false, backgroundColor: Colors.transparent, elevation: 0, - title: Obx( - () => Text( - 'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile', - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), + title: Text( + 'Complete Your Profile', + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), centerTitle: true, leading: IconButton( @@ -55,9 +54,19 @@ class FormRegistrationScreen extends StatelessWidget { ), ), body: Obx(() { - // Show loading while initializing - if (controller.selectedRole.value == null) { - return const Center(child: CircularProgressIndicator()); + // Make loading check more robust - showing a loading state while controller initializes + if (controller.userMetadata.value.userId == null && + controller.userMetadata.value.roleId == null) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Loading profile information..."), + ], + ), + ); } return SafeArea( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart index efcff58..a0add1a 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart @@ -209,14 +209,14 @@ class IdCardVerificationStep extends StatelessWidget { height: 180, width: double.infinity, decoration: BoxDecoration( - color: backgroundColor, // Using the dynamic background color + color: backgroundColor, borderRadius: BorderRadius.circular(TSizes.borderRadiusMd), border: Border.all( color: borderColor, width: controller.idCardError.value.isNotEmpty ? 2 - : 1, // Thicker border for error state + : 1, ), ), child: @@ -262,7 +262,7 @@ class IdCardVerificationStep extends StatelessWidget { ), const SizedBox(height: TSizes.xs), Text( - 'Tap to select an image', + 'Tap to select an image (max 4MB)', style: TextStyle( fontSize: TSizes.fontSizeSm, color: @@ -294,8 +294,7 @@ class IdCardVerificationStep extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular( - TSizes.borderRadiusMd - - 2, // Adjust for border width + TSizes.borderRadiusMd - 2, ), child: Image.file( File(controller.idCardImage.value!.path), @@ -313,8 +312,7 @@ class IdCardVerificationStep extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular( - TSizes.borderRadiusMd - - 2, // Adjust for border width + TSizes.borderRadiusMd - 2, ), color: TColors.error.withOpacity(0.2), ), @@ -341,7 +339,7 @@ class IdCardVerificationStep extends StatelessWidget { horizontal: TSizes.md, ), child: Text( - 'Please upload a clearer image', + controller.idCardError.value, textAlign: TextAlign.center, style: TextStyle( color: TColors.error, @@ -361,8 +359,7 @@ class IdCardVerificationStep extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular( - TSizes.borderRadiusMd - - 2, // Adjust for border width + TSizes.borderRadiusMd - 2, ), color: Colors.black.withOpacity(0.5), ), @@ -429,6 +426,37 @@ class IdCardVerificationStep extends StatelessWidget { minimumSize: const Size(double.infinity, 50), ), ), + // Show file size information if image is uploaded + if (controller.idCardImage.value != null) + FutureBuilder( + future: File(controller.idCardImage.value!.path).length(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final fileSizeKB = snapshot.data! / 1024; + final fileSizeMB = fileSizeKB / 1024; + final isOversized = + snapshot.data! > controller.maxFileSizeBytes; + return Padding( + padding: const EdgeInsets.only(top: TSizes.sm), + child: Text( + 'File size: ${fileSizeMB.toStringAsFixed(2)} MB', + style: TextStyle( + fontSize: TSizes.fontSizeSm, + color: + isOversized + ? TColors.error + : TColors.textSecondary, + fontWeight: + isOversized + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), ], ), ], @@ -443,7 +471,7 @@ class IdCardVerificationStep extends StatelessWidget { final String idCardType = isOfficer ? 'KTA' : 'KTP'; final String title = 'Select $idCardType Image Source'; final String message = - 'Please ensure your ID card is clear, well-lit, and all text is readable'; + 'Please ensure your ID card is clear, well-lit, and all text is readable. Maximum file size: 4MB'; Get.dialog( Dialog( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/selfie_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/selfie_verification_step.dart index 0f56860..2ac5e77 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/selfie_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/selfie_verification_step.dart @@ -327,7 +327,7 @@ class SelfieVerificationStep extends StatelessWidget { ), const SizedBox(height: TSizes.xs), Text( - 'Tap to open camera', + 'Tap to take a selfie (max 4MB)', style: TextStyle( fontSize: TSizes.fontSizeSm, color: @@ -517,6 +517,37 @@ class SelfieVerificationStep extends StatelessWidget { ), ], ), + // File size information + if (controller.selfieImage.value != null) + FutureBuilder( + future: File(controller.selfieImage.value!.path).length(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final fileSizeKB = snapshot.data! / 1024; + final fileSizeMB = fileSizeKB / 1024; + final isOversized = + snapshot.data! > controller.maxFileSizeBytes; + return Padding( + padding: const EdgeInsets.only(top: TSizes.sm), + child: Text( + 'File size: ${fileSizeMB.toStringAsFixed(2)} MB', + style: TextStyle( + fontSize: TSizes.fontSizeSm, + color: + isOversized + ? TColors.error + : TColors.textSecondary, + fontWeight: + isOversized + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), ], ), ], diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart index 4d2a33b..29dd991 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart @@ -8,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart'; +import 'package:sigap/src/shared/widgets/silver-app-bar/custom_silverbar.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/image_strings.dart'; @@ -34,134 +35,133 @@ class SignupWithRoleScreen extends StatelessWidget { return Scaffold( body: Obx( - () => Column( - children: [ - // Top section with image and role information - _buildTopImageSection(controller, context), - - // Bottom section with form - Expanded( - child: Container( - decoration: BoxDecoration( - color: isDark ? TColors.dark : TColors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(TSizes.borderRadiusLg), - topRight: Radius.circular(TSizes.borderRadiusLg), + () => NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + // Top image section as SliverAppBar + _buildSliverAppBar(controller, context), + + // Tab bar as pinned SliverPersistentHeader + SliverPersistentHeader( + delegate: TSliverTabBarDelegate( + child: _buildTabBar(context, controller), + minHeight: 70, // Height including padding + maxHeight: 70, // Fixed height for the tab bar + ), + pinned: true, + ), + ]; + }, + body: SafeArea( + top: false, + child: Container( + decoration: BoxDecoration( + color: isDark ? TColors.dark : TColors.white, + ), + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(TSizes.defaultSpace), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _buildSignupForm(context, controller), + ]), + ), ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, -5), + // Add extra padding at the bottom for safe area + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom, ), - ], - ), - child: Column( - children: [ - // Tab bar for switching between viewer and officer - _buildTabBar(context, controller), - - // Form content - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(TSizes.defaultSpace), - child: _buildSignupForm(context, controller), - ), - ), - ), - ], - ), + ), + ], ), ), - ], + ), ), ), ); } - Widget _buildTopImageSection( - SignupWithRoleController controller, - BuildContext context, + SliverAppBar _buildSliverAppBar( + SignupWithRoleController controller, + BuildContext context ) { bool isOfficer = controller.roleType.value == RoleType.officer; final isDark = THelperFunctions.isDarkMode(context); - - return Container( - height: - MediaQuery.of(context).size.height * - 0.35, // Take 35% of screen height - color: isDark ? TColors.dark : TColors.primary, - child: Stack( - children: [ - // Background gradient - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - isDark ? Colors.black : TColors.primary, - isDark ? TColors.dark : TColors.primary.withOpacity(0.8), - ], - ), - ), - ), - ), - - // Back button - Positioned( - top: MediaQuery.of(context).padding.top + TSizes.sm, - left: TSizes.sm, - child: GestureDetector( - onTap: () => Get.back(), + final topPadding = MediaQuery.of(context).padding.top; + + return SliverAppBar( + expandedHeight: MediaQuery.of(context).size.height * 0.35, + pinned: true, + backgroundColor: isDark ? TColors.dark : TColors.primary, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + // Background gradient + Positioned.fill( child: Container( - padding: const EdgeInsets.all(TSizes.sm), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + isDark ? Colors.black : TColors.primary, + isDark ? TColors.dark : TColors.primary.withOpacity(0.8), + ], + ), ), - child: const Icon(Icons.arrow_back, color: Colors.white), ), ), - ), - // Role image and text content - Align( - alignment: Alignment.center, - child: LayoutBuilder( - builder: (context, constraints) { - // Responsive image size based on available height/width - final double maxImageHeight = constraints.maxHeight * 0.9; - final double maxImageWidth = constraints.maxWidth * 0.9; - final double imageSize = - maxImageHeight < maxImageWidth - ? maxImageHeight - : maxImageWidth; + // Role image + Align( + alignment: Alignment.center, + child: LayoutBuilder( + builder: (context, constraints) { + // Responsive image size based on available height/width + final double maxImageHeight = constraints.maxHeight * 0.9; + final double maxImageWidth = constraints.maxWidth * 0.9; + final double imageSize = + maxImageHeight < maxImageWidth + ? maxImageHeight + : maxImageWidth; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Role image - SizedBox( - height: imageSize, - width: imageSize, - child: SvgPicture.asset( - isOfficer - ? (isDark - ? TImages.communicationDark - : TImages.communication) - : (isDark ? TImages.fallingDark : TImages.falling), - fit: BoxFit.contain, - ), + return SizedBox( + height: imageSize, + width: imageSize, + child: SvgPicture.asset( + isOfficer + ? (isDark + ? TImages.communicationDark + : TImages.communication) + : (isDark ? TImages.fallingDark : TImages.falling), + fit: BoxFit.contain, ), - ], - ); - }, + ); + }, + ), ), + ], + ), + ), + // Back button in the app bar + leading: Padding( + padding: EdgeInsets.only(top: topPadding * 0.2), + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + margin: const EdgeInsets.only(left: TSizes.sm), + padding: const EdgeInsets.all(TSizes.xs), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, color: Colors.white), ), - ], + ), ), ); } @@ -172,15 +172,24 @@ class SignupWithRoleScreen extends StatelessWidget { ) { final isDark = THelperFunctions.isDarkMode(context); - return Padding( + return Container( + decoration: BoxDecoration( + color: isDark ? TColors.dark : TColors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), padding: const EdgeInsets.fromLTRB( TSizes.defaultSpace, - TSizes.md, + TSizes.xs, TSizes.defaultSpace, - 0, + TSizes.xs, ), child: Container( - // Increase height from 50 to 60 or more height: 60, decoration: BoxDecoration( color: isDark ? TColors.darkContainer : TColors.lightContainer, @@ -425,6 +434,9 @@ class SignupWithRoleScreen extends StatelessWidget { ), ], ), + + // Add extra space at bottom for safety + SizedBox(height: MediaQuery.of(context).padding.bottom + 16), ], ), ); @@ -486,3 +498,5 @@ class SignupWithRoleScreen extends StatelessWidget { ); } } + + diff --git a/sigap-mobile/lib/src/features/auth/presentasion/step-form/step_form_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/step-form/step_form_screen.dart deleted file mode 100644 index fcb7a98..0000000 --- a/sigap-mobile/lib/src/features/auth/presentasion/step-form/step_form_screen.dart +++ /dev/null @@ -1,427 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter/services.dart'; -// import 'package:get/get.dart'; -// import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart'; -// import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; -// import 'package:sigap/src/features/personalization/data/models/index.dart'; -// import 'package:sigap/src/shared/widgets/dropdown/custom_dropdown.dart'; -// import 'package:sigap/src/shared/widgets/indicators/step_indicator/index.dart'; -// import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; -// import 'package:sigap/src/utils/constants/colors.dart'; -// import 'package:sigap/src/utils/validators/validation.dart'; - -// class FormRegistrationScreen extends StatelessWidget { -// const FormRegistrationScreen({super.key}); - -// @override -// Widget build(BuildContext context) { -// // Get the controller -// final controller = Get.find(); - -// // Set system overlay style -// SystemChrome.setSystemUIOverlayStyle( -// const SystemUiOverlayStyle( -// statusBarColor: Colors.transparent, -// statusBarIconBrightness: Brightness.dark, -// ), -// ); - -// return Scaffold( -// backgroundColor: TColors.light, -// appBar: AppBar( -// backgroundColor: Colors.transparent, -// elevation: 0, -// title: Obx( -// () => Text( -// 'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile', -// style: TextStyle( -// color: TColors.textPrimary, -// fontWeight: FontWeight.bold, -// ), -// ), -// ), -// centerTitle: true, -// leading: IconButton( -// icon: Icon(Icons.arrow_back, color: TColors.textPrimary), -// onPressed: () => Get.back(), -// ), -// ), -// body: Obx(() { -// // Show loading while initializing -// if (controller.selectedRole.value == null) { -// return const Center(child: CircularProgressIndicator()); -// } - -// return SafeArea( -// child: Column( -// children: [ -// // Step indicator -// Padding( -// padding: const EdgeInsets.all(24.0), -// child: Obx( -// () => StepIndicator( -// currentStep: controller.currentStep.value, -// totalSteps: controller.stepFormKeys.length, -// stepTitles: _getStepTitles(controller.selectedRole.value!), -// onStepTapped: controller.goToStep, -// ), -// ), -// ), - -// // Step content -// Expanded( -// child: SingleChildScrollView( -// child: Padding( -// padding: const EdgeInsets.all(24.0), -// child: Obx(() { -// return _buildStepContent(controller); -// }), -// ), -// ), -// ), - -// // Navigation buttons -// Padding( -// padding: const EdgeInsets.all(24.0), -// child: Row( -// children: [ -// // Back button -// Obx( -// () => -// controller.currentStep.value > 0 -// ? Expanded( -// child: Padding( -// padding: const EdgeInsets.only(right: 8.0), -// child: AuthButton( -// text: 'Previous', -// onPressed: controller.previousStep, -// isPrimary: false, -// ), -// ), -// ) -// : const SizedBox.shrink(), -// ), - -// // Next/Submit button -// Expanded( -// child: Padding( -// padding: EdgeInsets.only( -// left: controller.currentStep.value > 0 ? 8.0 : 0.0, -// ), -// child: Obx( -// () => AuthButton( -// text: -// controller.currentStep.value == -// controller.stepFormKeys.length - 1 -// ? 'Submit' -// : 'Next', -// onPressed: controller.nextStep, -// isLoading: controller.isLoading.value, -// ), -// ), -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ); -// }), -// ); -// } - -// List _getStepTitles(RoleModel role) { -// if (role.isOfficer) { -// return ['Personal', 'Officer Info', 'Unit Info']; -// } else { -// return ['Personal', 'Emergency']; -// } -// } - -// Widget _buildStepContent(FormRegistrationController controller) { -// final isOfficer = controller.selectedRole.value?.isOfficer ?? false; - -// switch (controller.currentStep.value) { -// case 0: -// return _buildPersonalInfoStep(controller); -// case 1: -// return isOfficer -// ? _buildOfficerInfoStep(controller) -// : _buildEmergencyContactStep(controller); -// case 2: -// // This step only exists for officers -// if (isOfficer) { -// return _buildOfficerAdditionalInfoStep(controller); -// } -// return const SizedBox.shrink(); -// default: -// return const SizedBox.shrink(); -// } -// } - -// Widget _buildPersonalInfoStep(FormRegistrationController controller) { -// return Form( -// key: controller.stepFormKeys[0], -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// 'Personal Information', -// style: TextStyle( -// fontSize: 20, -// fontWeight: FontWeight.bold, -// color: TColors.textPrimary, -// ), -// ), -// const SizedBox(height: 8), -// Text( -// 'Please provide your personal details', -// style: TextStyle(fontSize: 14, color: TColors.textSecondary), -// ), -// const SizedBox(height: 24), - -// // First Name field -// Obx( -// () => CustomTextField( -// label: 'First Name', -// controller: controller.firstNameController, -// validator: -// (value) => -// TValidators.validateUserInput('First name', value, 50), -// errorText: controller.firstNameError.value, -// textInputAction: TextInputAction.next, -// ), -// ), - -// // Last Name field -// Obx( -// () => CustomTextField( -// label: 'Last Name', -// controller: controller.lastNameController, -// validator: -// (value) => TValidators.validateUserInput( -// 'Last name', -// value, -// 50, -// required: false, -// ), -// errorText: controller.lastNameError.value, -// textInputAction: TextInputAction.next, -// ), -// ), - -// // Phone field -// Obx( -// () => CustomTextField( -// label: 'Phone Number', -// controller: controller.phoneController, -// validator: TValidators.validatePhoneNumber, -// errorText: controller.phoneError.value, -// keyboardType: TextInputType.phone, -// textInputAction: TextInputAction.next, -// ), -// ), - -// // Address field -// Obx( -// () => CustomTextField( -// label: 'Address', -// controller: controller.addressController, -// validator: -// (value) => -// TValidators.validateUserInput('Address', value, 255), -// errorText: controller.addressError.value, -// textInputAction: TextInputAction.done, -// maxLines: 3, -// ), -// ), -// ], -// ), -// ); -// } - -// Widget _buildEmergencyContactStep(FormRegistrationController controller) { -// return Form( -// key: controller.stepFormKeys[1], -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// 'Additional Information', -// style: TextStyle( -// fontSize: 20, -// fontWeight: FontWeight.bold, -// color: TColors.textPrimary, -// ), -// ), -// const SizedBox(height: 8), -// Text( -// 'Please provide additional personal details', -// style: TextStyle(fontSize: 14, color: TColors.textSecondary), -// ), -// const SizedBox(height: 24), - -// // NIK field -// Obx( -// () => CustomTextField( -// label: 'NIK (Identity Number)', -// controller: controller.nikController, -// validator: -// (value) => TValidators.validateUserInput('NIK', value, 16), -// errorText: controller.nikError.value, -// textInputAction: TextInputAction.next, -// keyboardType: TextInputType.number, -// ), -// ), - -// // Bio field -// Obx( -// () => CustomTextField( -// label: 'Bio', -// controller: controller.bioController, -// validator: -// (value) => TValidators.validateUserInput( -// 'Bio', -// value, -// 255, -// required: false, -// ), -// errorText: controller.bioError.value, -// textInputAction: TextInputAction.next, -// maxLines: 3, -// hintText: 'Tell us a little about yourself (optional)', -// ), -// ), - -// // Birth Date field -// Obx( -// () => CustomTextField( -// label: 'Birth Date (YYYY-MM-DD)', -// controller: controller.birthDateController, -// validator: -// (value) => -// TValidators.validateUserInput('Birth date', value, 10), -// errorText: controller.birthDateError.value, -// textInputAction: TextInputAction.done, -// keyboardType: TextInputType.datetime, -// hintText: 'e.g., 1990-01-31', -// ), -// ), -// ], -// ), -// ); -// } - -// Widget _buildOfficerInfoStep(FormRegistrationController controller) { -// return Form( -// key: controller.stepFormKeys[1], -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// 'Officer Information', -// style: TextStyle( -// fontSize: 20, -// fontWeight: FontWeight.bold, -// color: TColors.textPrimary, -// ), -// ), -// const SizedBox(height: 8), -// Text( -// 'Please provide your officer details', -// style: TextStyle(fontSize: 14, color: TColors.textSecondary), -// ), -// const SizedBox(height: 24), - -// // NRP field -// Obx( -// () => CustomTextField( -// label: 'NRP', -// controller: controller.nrpController, -// validator: TValidators.validateNRP, -// errorText: controller.nrpError.value, -// textInputAction: TextInputAction.next, -// ), -// ), - -// // Rank field -// Obx( -// () => CustomTextField( -// label: 'Rank', -// controller: controller.rankController, -// validator: TValidators.validateRank, -// errorText: controller.rankError.value, -// textInputAction: TextInputAction.done, -// ), -// ), -// ], -// ), -// ); -// } - -// Widget _buildOfficerAdditionalInfoStep( -// FormRegistrationController controller, -// ) { -// return Form( -// key: controller.stepFormKeys[2], -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// 'Unit Information', -// style: TextStyle( -// fontSize: 20, -// fontWeight: FontWeight.bold, -// color: TColors.textPrimary, -// ), -// ), -// const SizedBox(height: 8), -// Text( -// 'Please provide your unit details', -// style: TextStyle(fontSize: 14, color: TColors.textSecondary), -// ), -// const SizedBox(height: 24), - -// // Position field -// Obx( -// () => CustomTextField( -// label: 'Position', -// controller: controller.positionController, -// validator: TValidators.validatePosition, -// errorText: controller.positionError.value, -// textInputAction: TextInputAction.next, -// ), -// ), - -// // Unit Dropdown -// Obx( -// () => CustomDropdown( -// label: 'Unit', -// items: -// controller.availableUnits -// .map( -// (unit) => DropdownMenuItem( -// value: unit.codeUnit, -// child: Text(unit.name), -// ), -// ) -// .toList(), -// value: -// controller.unitIdController.text.isEmpty -// ? null -// : controller.unitIdController.text, -// onChanged: (value) { -// if (value != null) { -// controller.unitIdController.text = value.toString(); -// } -// }, -// validator: (value) => TValidators.validateUnitId(value), -// errorText: controller.unitIdError.value, -// ), -// ), -// ], -// ), -// ); -// } -// } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart index 6cd4f35..82d4caf 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart @@ -46,59 +46,63 @@ class PasswordField extends StatelessWidget { ), ), const SizedBox(height: TSizes.sm), - TextFormField( - controller: controller, - validator: validator, - obscureText: !isVisible.value, - textInputAction: textInputAction, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: isDark ? TColors.white : TColors.textPrimary, - ), - decoration: InputDecoration( - hintText: hintText, - hintStyle: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), - errorText: - errorText != null && errorText!.isNotEmpty ? errorText : null, - errorStyle: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: TColors.error), - contentPadding: const EdgeInsets.symmetric( - horizontal: TSizes.md, - vertical: TSizes.md, + Obx( + () => + TextFormField( + controller: controller, + validator: validator, + obscureText: !isVisible.value, + textInputAction: textInputAction, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: isDark ? TColors.white : TColors.textPrimary, ), - prefixIcon: prefixIcon, - suffixIcon: IconButton( - onPressed: onToggleVisibility, - icon: Icon( - isVisible.value ? Icons.visibility_off : Icons.visibility, - color: effectiveAccentColor, - semanticLabel: - isVisible.value ? 'Hide password' : 'Show password', + decoration: InputDecoration( + hintText: hintText, + hintStyle: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), + errorText: + errorText != null && errorText!.isNotEmpty ? errorText : null, + errorStyle: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: TColors.error), + contentPadding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + prefixIcon: prefixIcon, + suffixIcon: IconButton( + onPressed: onToggleVisibility, + icon: Icon( + isVisible.value ? Icons.visibility_off : Icons.visibility, + color: effectiveAccentColor, + semanticLabel: + isVisible.value ? 'Hide password' : 'Show password', + ), + ), + filled: true, + fillColor: + isDark ? TColors.darkContainer : TColors.lightContainer, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.borderPrimary, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.borderPrimary, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: effectiveAccentColor, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.error, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.error, width: 1.5), ), - ), - filled: true, - fillColor: isDark ? TColors.darkContainer : TColors.lightContainer, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: TColors.borderPrimary, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: TColors.borderPrimary, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: effectiveAccentColor, width: 1.5), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: TColors.error, width: 1), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: TColors.error, width: 1.5), ), ), ), diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart index cde44ce..ab636d7 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart @@ -112,10 +112,13 @@ class OnboardingController extends GetxController // If location is valid, proceed to role selection Get.offAllNamed(AppRoutes.signupWithRole); - TLoaders.successSnackBar( - title: 'Location Valid', - message: 'Checking location was successful', - ); + // TLoaders.successSnackBar( + // title: 'Location Valid', + // message: 'Checking location was successful', + // ); + + // Store isfirstTime to false in storage + _storage.write('isFirstTime', false); } else { // If location is invalid, show warning screen Get.offAllNamed(AppRoutes.locationWarning); diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart index a1c3a66..fc0743a 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; +import 'package:sigap/src/utils/constants/image_strings.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; class LocationWarningScreen extends StatelessWidget { @@ -27,10 +29,14 @@ class LocationWarningScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ // Warning Icon - use theme error color - Icon( - Icons.location_off, - size: 80, - color: theme.colorScheme.error, + Lottie.asset( + TImages.unverifyLocationAnimation, + width: 200, + height: 200, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error, size: 100, color: Colors.red); + }, ), const SizedBox(height: TSizes.spaceBtwSections), diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart index 46ee791..2937b92 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart @@ -9,7 +9,6 @@ import '../../controllers/onboarding_controller.dart'; class OnboardingScreen extends StatelessWidget { const OnboardingScreen({super.key}); - @override Widget build(BuildContext context) { // Get the controller diff --git a/sigap-mobile/lib/src/shared/widgets/silver-app-bar/custom_silverbar.dart b/sigap-mobile/lib/src/shared/widgets/silver-app-bar/custom_silverbar.dart new file mode 100644 index 0000000..6d569bc --- /dev/null +++ b/sigap-mobile/lib/src/shared/widgets/silver-app-bar/custom_silverbar.dart @@ -0,0 +1,36 @@ +// Custom SliverPersistentHeaderDelegate for the tab bar +import 'package:flutter/widgets.dart'; + +class TSliverTabBarDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + final double minHeight; + final double maxHeight; + + TSliverTabBarDelegate({ + required this.child, + required this.minHeight, + required this.maxHeight, + }); + + @override + double get minExtent => minHeight; + + @override + double get maxExtent => maxHeight; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return SizedBox.expand(child: child); + } + + @override + bool shouldRebuild(TSliverTabBarDelegate oldDelegate) { + return maxHeight != oldDelegate.maxHeight || + minHeight != oldDelegate.minHeight || + child != oldDelegate.child; + } +} diff --git a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart index a434dd5..cbc4768 100644 --- a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart +++ b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart @@ -71,10 +71,7 @@ class CustomTextField extends StatelessWidget { context, ).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), errorText: - errorText != null && errorText!.isNotEmpty ? errorText : null, - errorStyle: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: TColors.error), + errorText != null && errorText!.isNotEmpty ? errorText : null, contentPadding: const EdgeInsets.symmetric( horizontal: TSizes.md, vertical: TSizes.md, @@ -105,7 +102,17 @@ class CustomTextField extends StatelessWidget { ), ), ), - const SizedBox(height: TSizes.spaceBtwInputFields), + // if (errorText != null && errorText!.isNotEmpty) + // Padding( + // padding: const EdgeInsets.only(top: 6.0), + // child: Text( + // errorText!, + // style: Theme.of( + // context, + // ).textTheme.bodySmall?.copyWith(color: TColors.error), + // ), + // ), + // const SizedBox(height: TSizes.spaceBtwInputFields), ], ); } diff --git a/sigap-mobile/lib/src/utils/constants/form_key.dart b/sigap-mobile/lib/src/utils/constants/form_key.dart new file mode 100644 index 0000000..b0293dd --- /dev/null +++ b/sigap-mobile/lib/src/utils/constants/form_key.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +/// Class containing all the FormKey identifiers used across the app +/// This helps prevent duplication of form key strings and makes it easier to manage them +class TGlobalFormKey { + // Registration form step keys + static const String personalInfoForm = 'personal_info_new_form_key'; + static const String idCardVerificationForm = 'id_card_verification_form_key'; + static const String selfieVerificationForm = 'selfie_verification_form_key'; + static const String identityVerificationForm = + 'identity_verification_form_key'; + static const String officerInfoForm = 'officer_info_form_key'; + static const String unitInfoForm = 'unit_info_form_key'; + + // Authentication form keys + static const String signInForm = 'sign_in_form_key'; + static const String signUpForm = 'sign_up_form_key'; + static const String forgotPasswordForm = 'forgot_password_form_key'; + static const String resetPasswordForm = 'reset_password_form_key'; + static const String otpVerificationForm = 'otp_verification_form_key'; + + // Profile form keys + static const String editProfileForm = 'edit_profile_form_key'; + static const String changePasswordForm = 'change_password_form_key'; + static const String notificationSettingsForm = + 'notification_settings_form_key'; + + // Create global keys for forms with specific debug labels + static GlobalKey createFormKey(String keyName) { + return GlobalKey(debugLabel: keyName); + } + + // Helper function to get a personal info form key + static GlobalKey personalInfo() { + return createFormKey(personalInfoForm); + } + + // Helper function to get an ID card verification form key + static GlobalKey idCardVerification() { + return createFormKey(idCardVerificationForm); + } + + // Helper function to get a selfie verification form key + static GlobalKey selfieVerification() { + return createFormKey(selfieVerificationForm); + } + + // Helper function to get an identity verification form key + static GlobalKey identityVerification() { + return createFormKey(identityVerificationForm); + } + + // Helper function to get an officer info form key + static GlobalKey officerInfo() { + return createFormKey(officerInfoForm); + } + + // Helper function to get a unit info form key + static GlobalKey unitInfo() { + return createFormKey(unitInfoForm); + } +} diff --git a/sigap-mobile/lib/src/utils/constants/image_strings.dart b/sigap-mobile/lib/src/utils/constants/image_strings.dart index 9b3e7de..285bd8a 100644 --- a/sigap-mobile/lib/src/utils/constants/image_strings.dart +++ b/sigap-mobile/lib/src/utils/constants/image_strings.dart @@ -174,6 +174,8 @@ class TImages { "assets/images/animations/splash-dark.json"; static const String splashLightAnimation = "assets/images/animations/splash-light.json"; + static const String unverifyLocationAnimation = + "assets/images/animations/empty-address.json"; // -- Content Images (assets/images/content) static const String backpackingDark = diff --git a/sigap-mobile/lib/src/utils/constants/num_int.dart b/sigap-mobile/lib/src/utils/constants/num_int.dart index 84e0f5a..0259eb9 100644 --- a/sigap-mobile/lib/src/utils/constants/num_int.dart +++ b/sigap-mobile/lib/src/utils/constants/num_int.dart @@ -1,4 +1,6 @@ -class DNum { +class TNum { // Auth Number static const int oneTimePassword = 6; + static const int totalStepViewer = 4; + static const int totalStepOfficer = 5; } diff --git a/sigap-mobile/lib/src/utils/popups/loaders.dart b/sigap-mobile/lib/src/utils/popups/loaders.dart index 201b9a8..be2db3f 100644 --- a/sigap-mobile/lib/src/utils/popups/loaders.dart +++ b/sigap-mobile/lib/src/utils/popups/loaders.dart @@ -9,81 +9,104 @@ class TLoaders { static hideSnackBar() => ScaffoldMessenger.of(Get.context!).hideCurrentSnackBar(); static customToast({required message}) { - ScaffoldMessenger.of(Get.context!).showSnackBar( - SnackBar( - elevation: 0, - duration: const Duration(seconds: 3), - backgroundColor: Colors.transparent, - content: Container( - padding: const EdgeInsets.all(12.0), - margin: const EdgeInsets.symmetric(horizontal: 30), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: THelperFunctions.isDarkMode(Get.context!) ? TColors.darkerGrey.withOpacity(0.9) : TColors.grey.withOpacity(0.9), + // Use post-frame callback for any UI updates + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(Get.context!).showSnackBar( + SnackBar( + elevation: 0, + duration: const Duration(seconds: 3), + backgroundColor: Colors.transparent, + content: Container( + padding: const EdgeInsets.all(12.0), + margin: const EdgeInsets.symmetric(horizontal: 30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: + THelperFunctions.isDarkMode(Get.context!) + ? TColors.darkerGrey.withOpacity(0.9) + : TColors.grey.withOpacity(0.9), + ), + child: Center( + child: Text( + message, + style: Theme.of(Get.context!).textTheme.labelLarge, + ), + ), ), - child: Center(child: Text(message, style: Theme.of(Get.context!).textTheme.labelLarge)), ), - ), - ); + ); + }); } static successSnackBar({required title, message = '', duration = 3}) { - Get.snackbar( - title, - message, - isDismissible: true, - shouldIconPulse: true, - colorText: Colors.white, - backgroundColor: TColors.primary, - snackPosition: SnackPosition.TOP, - duration: Duration(seconds: duration), - margin: const EdgeInsets.all(10), - icon: const Icon(Iconsax.check, color: TColors.white), - ); + // Use post-frame callback for any UI updates + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.snackbar( + title, + message, + isDismissible: true, + shouldIconPulse: true, + colorText: Colors.white, + backgroundColor: TColors.primary, + snackPosition: SnackPosition.TOP, + duration: Duration(seconds: duration), + margin: const EdgeInsets.all(10), + icon: const Icon(Iconsax.check, color: TColors.white), + ); + }); } static warningSnackBar({required title, message = ''}) { - Get.snackbar( - title, - message, - isDismissible: true, - shouldIconPulse: true, - colorText: TColors.white, - backgroundColor: Colors.orange, - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - margin: const EdgeInsets.all(20), - icon: const Icon(Iconsax.warning_2, color: TColors.white), - ); + // Use post-frame callback for any UI updates + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.snackbar( + title, + message, + isDismissible: true, + shouldIconPulse: true, + colorText: TColors.white, + backgroundColor: Colors.orange, + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + margin: const EdgeInsets.all(20), + icon: const Icon(Iconsax.warning_2, color: TColors.white), + ); + }); } static errorSnackBar({required title, message = ''}) { - Get.snackbar( - title, - message, - isDismissible: true, - shouldIconPulse: true, - colorText: TColors.white, - backgroundColor: Colors.red.shade600, - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - margin: const EdgeInsets.all(20), - icon: const Icon(Iconsax.warning_2, color: TColors.white), - ); + // Use post-frame callback for any UI updates + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.snackbar( + title, + message, + isDismissible: true, + shouldIconPulse: true, + colorText: TColors.white, + backgroundColor: Colors.red.shade600, + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + margin: const EdgeInsets.all(20), + icon: const Icon(Iconsax.warning_2, color: TColors.white), + ); + }); } static infoSnackBar({required title, message = ''}) { - Get.snackbar( - title, - message, - isDismissible: true, - shouldIconPulse: true, - colorText: TColors.white, - backgroundColor: Colors.blue.shade600, - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - margin: const EdgeInsets.all(20), - icon: const Icon(Iconsax.info_circle, color: TColors.white), - ); + // Use post-frame callback for any UI updates + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.snackbar( + title, + message, + isDismissible: true, + shouldIconPulse: true, + colorText: TColors.white, + backgroundColor: Colors.blue.shade600, + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + margin: const EdgeInsets.all(20), + icon: const Icon(Iconsax.info_circle, color: TColors.white), + ); + }); } } diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index c39c6ef..fea1150 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -13,7 +13,6 @@ datasource db { model profiles { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid user_id String @unique @db.Uuid - nik String @unique @default("") @db.VarChar(100) avatar String? @db.VarChar(355) username String? @unique @db.VarChar(255) first_name String? @db.VarChar(255) @@ -21,9 +20,9 @@ model profiles { bio String? @db.VarChar address Json? @db.Json birth_date DateTime? + nik String? @default("") @db.VarChar(100) users users @relation(fields: [user_id], references: [id]) - @@index([nik], map: "idx_profiles_nik") @@index([user_id]) @@index([username]) } @@ -43,19 +42,19 @@ model users { user_metadata Json? created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) - is_banned Boolean @default(false) - spoofing_attempts Int @default(0) - panic_strike Int @default(0) - banned_reason String? @db.VarChar(255) banned_until DateTime? @db.Timestamptz(6) is_anonymous Boolean @default(false) + banned_reason String? @db.VarChar(255) + is_banned Boolean @default(false) + panic_strike Int @default(0) + spoofing_attempts Int @default(0) events events[] incident_logs incident_logs[] location_logs location_logs[] + panic_button_logs panic_button_logs[] profile profiles? sessions sessions[] role roles @relation(fields: [roles_id], references: [id]) - panic_button_logs panic_button_logs[] @@index([is_anonymous]) @@index([created_at]) @@ -68,9 +67,9 @@ model roles { description String? created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) + officers officers[] permissions permissions[] users users[] - officers officers[] } model sessions { @@ -267,10 +266,10 @@ model incident_logs { verified Boolean? @default(false) created_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6) + evidence evidence[] crime_categories crime_categories @relation(fields: [category_id], references: [id], map: "fk_incident_category") locations locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction) user users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) - evidence evidence[] panic_button_logs panic_button_logs[] @@index([category_id], map: "idx_incident_logs_category_id") @@ -278,16 +277,15 @@ model incident_logs { } model evidence { - id String @id @unique @db.VarChar(20) - incident_id String @db.Uuid - type String @db.VarChar(50) // contoh: photo, video, document, images - url String @db.Text - description String? @db.VarChar(255) - caption String? @db.VarChar(255) + incident_id String @db.Uuid + type String @db.VarChar(50) + url String + uploaded_at DateTime? @default(now()) @db.Timestamptz(6) + caption String? @db.VarChar(255) + description String? @db.VarChar(255) metadata Json? - uploaded_at DateTime? @default(now()) @db.Timestamptz(6) - - incident incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade) + id String @id @unique @db.VarChar(20) + incident incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade) @@index([incident_id], map: "idx_evidence_incident_id") } @@ -307,11 +305,11 @@ model units { location Unsupported("geography") city_id String @db.VarChar(20) phone String? @db.VarChar(20) + officers officers[] + patrol_units patrol_units[] unit_statistics unit_statistics[] cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction) districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction) - officers officers[] - patrol_units patrol_units[] @@index([name], map: "idx_units_name") @@index([type], map: "idx_units_type") @@ -320,24 +318,20 @@ model units { @@index([location], map: "idx_unit_location", type: Gist) @@index([district_id, location], map: "idx_units_location_district") @@index([location], map: "idx_units_location_gist", type: Gist) - @@index([location], type: Gist) - @@index([location], map: "units_location_idx1", type: Gist) - @@index([location], map: "units_location_idx2", type: Gist) } model patrol_units { - id String @id @unique @db.VarChar(100) - unit_id String @db.VarChar(20) - location_id String @db.Uuid - name String @db.VarChar(100) - type String @db.VarChar(50) - status String @db.VarChar(50) + unit_id String @db.VarChar(20) + location_id String @db.Uuid + name String @db.VarChar(100) + type String @db.VarChar(50) + status String @db.VarChar(50) radius Float - created_at DateTime @default(now()) @db.Timestamptz(6) - - members officers[] - location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction) - unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) + created_at DateTime @default(now()) @db.Timestamptz(6) + id String @id @unique @db.VarChar(100) + members officers[] + location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) @@index([unit_id], map: "idx_patrol_units_unit_id") @@index([location_id], map: "idx_patrol_units_location_id") @@ -347,10 +341,8 @@ model patrol_units { } model officers { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid unit_id String @db.VarChar(20) role_id String @db.Uuid - patrol_unit_id String @db.VarChar(100) nrp String @unique @db.VarChar(100) name String @db.VarChar(100) rank String? @db.VarChar(100) @@ -360,16 +352,20 @@ model officers { avatar String? valid_until DateTime? qr_code String? + created_at DateTime? @default(now()) @db.Timestamptz(6) + updated_at DateTime? @default(now()) @db.Timestamptz(6) + patrol_unit_id String @db.VarChar(100) + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + banned_reason String? @db.VarChar(255) + banned_until DateTime? is_banned Boolean @default(false) panic_strike Int @default(0) spoofing_attempts Int @default(0) - banned_reason String? @db.VarChar(255) - banned_until DateTime? - created_at DateTime? @default(now()) @db.Timestamptz(6) - updated_at DateTime? @default(now()) @db.Timestamptz(6) - units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) + place_of_birth String? + date_of_birth DateTime? @db.Timestamptz(6) + patrol_units patrol_units @relation(fields: [patrol_unit_id], references: [id]) roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction) - patrol_units patrol_units? @relation(fields: [patrol_unit_id], references: [id]) + units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) panic_button_logs panic_button_logs[] @@index([unit_id], map: "idx_officers_unit_id") @@ -453,9 +449,9 @@ model panic_button_logs { officer_id String? @db.Uuid incident_id String @db.Uuid timestamp DateTime @db.Timestamptz(6) - users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) - officers officers? @relation(fields: [officer_id], references: [id], onDelete: Cascade, onUpdate: NoAction) incidents incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + officers officers? @relation(fields: [officer_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) @@index([user_id], map: "idx_panic_buttons_user_id") }