1313from src .core .time import _utc_now
1414
1515
16- class AnalitikItem (BaseModel ):
17- """Individual analytics item with type, percentage, and total."""
16+ class PeringkatItem (BaseModel ):
17+ """Breakdown of peringkat distribution within a jenis category."""
18+
19+ peringkat : str = Field (..., description = "Peringkat (e.g., Rendah, Menengah)" )
20+ total : int = Field (..., description = "Jumlah sekolah untuk peringkat ini dalam jenis ini" )
21+
22+
23+ class AnalitikJenisItem (BaseModel ):
24+ """Analytics item for jenisLabel dimension."""
25+
26+ jenis : str = Field (..., description = "Kategori atau jenis" )
27+ peratus : float = Field (..., description = "Peratusan daripada jumlah keseluruhan" )
28+ total : int = Field (..., description = "Jumlah bilangan untuk kategori ini" )
29+ peringkatBreakdown : list [PeringkatItem ] = Field (default_factory = list , description = "Breakdown mengikut peringkat dalam jenis ini" )
30+
31+
32+ class AnalitikBantuanItem (BaseModel ):
33+ """Analytics item for bantuan dimension."""
1834
1935 jenis : str = Field (..., description = "Kategori atau jenis" )
2036 peratus : float = Field (..., description = "Peratusan daripada jumlah keseluruhan" )
@@ -24,8 +40,8 @@ class AnalitikItem(BaseModel):
2440class AnalitikSekolahData (BaseModel ):
2541 """Container for all analytics dimensions in array format."""
2642
27- jenisLabel : list [AnalitikItem ] = Field (default_factory = list , description = "Analisis mengikut jenis/label sekolah" )
28- bantuan : list [AnalitikItem ] = Field (default_factory = list , description = "Analisis mengikut jenis bantuan" )
43+ jenisLabel : list [AnalitikJenisItem ] = Field (default_factory = list , description = "Analisis mengikut jenis/label sekolah" )
44+ bantuan : list [AnalitikBantuanItem ] = Field (default_factory = list , description = "Analisis mengikut jenis bantuan" )
2945
3046
3147_settings = get_settings ()
@@ -55,12 +71,16 @@ def from_sekolah_list(cls, sekolah_list: list["Sekolah"]) -> "AnalitikSekolah":
5571
5672 # Initialize analytics containers with defaultdicts for dynamic counting
5773 jenis_counts = defaultdict (int )
74+ jenis_peringkat_counts = defaultdict (lambda : defaultdict (int ))
5875 bantuan_counts = defaultdict (int )
5976
6077 # Process each sekolah to build analytics dynamically
6178 for sekolah in sekolah_list :
6279 # Count categories
63- cls ._increment_count (jenis_counts , sekolah .jenisLabel )
80+ jenis_key = cls ._normalize_value (sekolah .jenisLabel )
81+ peringkat_key = cls ._normalize_peringkat_value (sekolah .peringkat )
82+ jenis_counts [jenis_key ] += 1
83+ jenis_peringkat_counts [jenis_key ][peringkat_key ] += 1
6484 cls ._increment_count (bantuan_counts , sekolah .bantuan )
6585
6686 # Sum totals
@@ -77,10 +97,14 @@ def from_sekolah_list(cls, sekolah_list: list["Sekolah"]) -> "AnalitikSekolah":
7797 student_count += sekolah .enrolmenKhas
7898 total_pelajar += student_count
7999
80- # Convert counts to AnalitikItem arrays
100+ # Convert counts to analytics arrays
81101 data = AnalitikSekolahData (
82- jenisLabel = cls ._convert_to_analitik_items (jenis_counts , jumlah_sekolah ),
83- bantuan = cls ._convert_to_analitik_items (bantuan_counts , jumlah_sekolah ),
102+ jenisLabel = cls ._convert_to_analitik_jenis_items (
103+ jenis_counts ,
104+ jumlah_sekolah ,
105+ jenis_peringkat_counts = jenis_peringkat_counts ,
106+ ),
107+ bantuan = cls ._convert_to_analitik_bantuan_items (bantuan_counts , jumlah_sekolah ),
84108 )
85109
86110 return cls (
@@ -98,8 +122,12 @@ def _increment_count(counter: dict, value: Optional[str]) -> None:
98122 counter [key ] = counter .get (key , 0 ) + 1
99123
100124 @staticmethod
101- def _convert_to_analitik_items (counter : defaultdict , total : int ) -> list [AnalitikItem ]:
102- """Convert defaultdict to list of AnalitikItem objects."""
125+ def _convert_to_analitik_jenis_items (
126+ counter : defaultdict ,
127+ total : int ,
128+ jenis_peringkat_counts : Optional [dict [str , dict ]] = None ,
129+ ) -> list [AnalitikJenisItem ]:
130+ """Convert defaultdict to list of AnalitikJenisItem objects."""
103131 items = []
104132
105133 # Add existing categories
@@ -109,32 +137,106 @@ def _convert_to_analitik_items(counter: defaultdict, total: int) -> list[Analiti
109137 else :
110138 peratus = 0.0
111139
112- items .append (AnalitikItem (
140+ # Build peringkat breakdown for this jenis if data is available
141+ peringkat_breakdown = []
142+ if jenis_peringkat_counts and jenis in jenis_peringkat_counts :
143+ peringkat_counts = jenis_peringkat_counts [jenis ]
144+ for peringkat_key , peringkat_count in peringkat_counts .items ():
145+ if peringkat_key != "TIADA MAKLUMAT" : # Only include actual peringkat values
146+ peringkat_breakdown .append (
147+ PeringkatItem (
148+ peringkat = AnalitikSekolah ._display_peringkat (peringkat_key ),
149+ total = peringkat_count
150+ )
151+ )
152+ # Add TIADA MAKLUMAT if it exists
153+ if "TIADA MAKLUMAT" in peringkat_counts and peringkat_counts ["TIADA MAKLUMAT" ] > 0 :
154+ peringkat_breakdown .append (
155+ PeringkatItem (
156+ peringkat = "TIADA MAKLUMAT" ,
157+ total = peringkat_counts ["TIADA MAKLUMAT" ]
158+ )
159+ )
160+ # Sort by total descending
161+ peringkat_breakdown .sort (key = lambda x : x .total , reverse = True )
162+
163+ items .append (AnalitikJenisItem (
113164 jenis = jenis ,
114165 peratus = peratus ,
115- total = count
166+ total = count ,
167+ peringkatBreakdown = peringkat_breakdown
116168 ))
117169
118170 # Ensure "Tiada Maklumat" exists even if count is 0
119171 tiada_maklumat_exists = any (item .jenis == "TIADA MAKLUMAT" for item in items )
120172 if not tiada_maklumat_exists :
121- items .append (AnalitikItem (
173+ items .append (AnalitikJenisItem (
122174 jenis = "TIADA MAKLUMAT" ,
123175 peratus = 0.0 ,
124- total = 0
176+ total = 0 ,
177+ peringkatBreakdown = []
125178 ))
126179
127180 # Sort by total count descending
128181 items .sort (key = lambda x : x .total , reverse = True )
129182 return items
130183
184+ @staticmethod
185+ def _convert_to_analitik_bantuan_items (
186+ counter : defaultdict ,
187+ total : int ,
188+ ) -> list [AnalitikBantuanItem ]:
189+ """Convert defaultdict to list of AnalitikBantuanItem objects."""
190+ items = []
191+
192+ for jenis , count in counter .items ():
193+ if total > 0 :
194+ peratus = round ((count / total ) * 100 , 1 )
195+ else :
196+ peratus = 0.0
197+
198+ items .append (AnalitikBantuanItem (
199+ jenis = jenis ,
200+ peratus = peratus ,
201+ total = count ,
202+ ))
203+
204+ tiada_maklumat_exists = any (item .jenis == "TIADA MAKLUMAT" for item in items )
205+ if not tiada_maklumat_exists :
206+ items .append (AnalitikBantuanItem (
207+ jenis = "TIADA MAKLUMAT" ,
208+ peratus = 0.0 ,
209+ total = 0 ,
210+ ))
211+
212+ items .sort (key = lambda x : x .total , reverse = True )
213+ return items
214+
131215 @staticmethod
132216 def _normalize_value (value : Optional [str ]) -> str :
133217 """Normalize field values for consistent analytics categorization."""
134218 if value is None or str (value ).strip () == "" :
135219 return "TIADA MAKLUMAT"
136220 return str (value ).strip ().upper ()
137221
222+ @classmethod
223+ def _normalize_peringkat_value (cls , value : Optional [str ]) -> str :
224+ if value is not None and hasattr (value , "value" ):
225+ value = getattr (value , "value" )
226+ normalized = cls ._normalize_value (value )
227+ if normalized in {"RENDAH" , "MENENGAH" }:
228+ return normalized
229+ return "TIADA MAKLUMAT"
230+
231+ @classmethod
232+ def _display_peringkat (cls , value : str ) -> str :
233+ normalized = cls ._normalize_peringkat_value (value )
234+ if normalized == "RENDAH" :
235+ return "RENDAH"
236+ if normalized == "MENENGAH" :
237+ return "MENENGAH"
238+ return "TIADA MAKLUMAT"
239+
138240 def to_document (self ) -> dict :
139241 """Convert the analytics to a Mongo-ready document, omitting None fields."""
140242 doc = self .model_dump (exclude_none = True , by_alias = True )
@@ -145,5 +247,7 @@ def to_document(self) -> dict:
145247__all__ = [
146248 "AnalitikSekolah" ,
147249 "AnalitikSekolahData" ,
148- "AnalitikItem" ,
250+ "AnalitikJenisItem" ,
251+ "AnalitikBantuanItem" ,
252+ "PeringkatItem" ,
149253]
0 commit comments